hitl-proxy
# hitl-proxy
> **Human-in-the-Loop MCP Server for LLM Agents**
A Model Context Protocol (MCP) server that **enforces human approval** before any LLM agent can edit files, create files, or execute shell commands.
Works with any MCP-compatible IDE: opencode, Cursor, Windsurf, VS Code Copilot, and others.
---
## The Problem It Solves
LLMs are trained to be helpful and complete tasks — which means they tend to **assume, infer, and act** without checking with the human first. In a coding assistant context, this leads to:
- Unreviewed file edits
- Destructive commands run without warning
- Assumed context that was never verified
`hitl-proxy` inserts a **mandatory human checkpoint** before every write operation.
---
## How It Works
```
LLM wants to edit a file
│
▼
edit_hitl({ ..., approved: false }) ← First call
│
▼
HITL Proxy blocks + issues sessionToken
Returns: "Use question() to ask the user. Token: abc-123"
│
▼
LLM uses question() → human sees options → human approves
│
▼
edit_hitl({ ..., approved: true, sessionToken: "abc-123" }) ← Second call
│
▼
HITL Proxy validates token → executes edit → logs to audit file
```
---
## Enforcement Mechanisms
| Mechanism | Description |
|---|---|
| **Session Tokens** | `approved: true` alone is not enough. LLM must present a valid single-use token issued during the block phase |
| **Violation Counter** | Counts how many times the LLM tried to bypass HITL. Escalates warning messages at 2+ violations |
| **declare_intent_hitl** | Optional tool for the LLM to declare intent *before* asking the user. Best practice flow |
| **Self-check Block** | Every blocked response includes a mandatory self-evaluation prompt for the LLM |
| **Audit Log** | NDJSON log of every action (approved or blocked) with timestamp and metadata |
| **Path Traversal Protection** | File paths are validated against `HITL_PROJECT_ROOT` to prevent access outside the project |
| **Cross-platform Bash** | Uses `cmd /c` on Windows, `sh -c` on Unix. Configurable timeout |
---
## Installation
```bash
# In your project directory
mkdir hitl-proxy
cd hitl-proxy
# Copy src/index.js and package.json from this repo
npm install
```
---
## Configuration
### opencode.json
```json
{
"mcp": {
"hitl-proxy": {
"type": "local",
"command": ["node", "./hitl-proxy/src/index.js"],
"enabled": true
}
},
"permission": {
"edit": "deny",
"write": "deny"
}
}
```
> **Critical:** The `"edit": "deny"` and `"write": "deny"` permissions are **mandatory**.
> Without them, the LLM will use the native IDE tools and bypass the proxy entirely.
See `config/opencode.example.json` for a full example.
### Environment Variables
| Variable | Default | Description |
|---|---|---|
| `HITL_PROJECT_ROOT` | `process.cwd()` | Root directory. File paths are validated against this |
| `HITL_AUDIT_LOG` | `./hitl-audit.log` | Path to the NDJSON audit log file |
| `HITL_TOKEN_TTL` | `300000` (5 min) | Session token TTL in milliseconds |
| `HITL_BASH_TIMEOUT` | `30000` (30 sec) | Shell command timeout in milliseconds |
| `HITL_QUESTION_TOOL` | `question` | Name of the IDE's human-input tool |
Set them in your MCP server command:
```json
"command": ["node", "./hitl-proxy/src/index.js"],
"env": {
"HITL_PROJECT_ROOT": "/path/to/your/project",
"HITL_TOKEN_TTL": "600000"
}
```
---
## Tools Reference
### `declare_intent_hitl` *(best practice — call before question())*
Declares what the LLM intends to do and why. Returns a `sessionToken`.
```
Parameters:
action — "edit" | "write" | "bash"
target — file path or command
reason — why this action is needed now
```
### `edit_hitl`
Edits an existing file by replacing a text fragment.
```
Parameters:
filePath — file to edit
oldString — exact text to replace
newString — replacement text
replaceAll — (optional) replace all occurrences, not just first
approved — true only if user approved via question()
sessionToken — token from block response or declare_intent_hitl
```
### `write_hitl`
Creates or overwrites a file.
```
Parameters:
filePath — file to create
content — full file content
approved — true only if user approved via question()
sessionToken — token from block response or declare_intent_hitl
```
### `bash_hitl`
Executes a shell command.
```
Parameters:
command — shell command to run
approved — true only if user approved via question()
sessionToken — token from block response or declare_intent_hitl
```
---
## Ideal LLM Workflow (Best Practice)
```
1. declare_intent_hitl({ action: "edit", target: "src/app.js", reason: "Fix typo in error message" })
→ receives sessionToken: "uuid-xxx"
2. question("¿Apruebas editar src/app.js para corregir el mensaje de error?", options: ["✅ Sí", "❌ No"])
→ user selects "✅ Sí"
3. edit_hitl({ filePath: "src/app.js", oldString: "...", newString: "...", approved: true, sessionToken: "uuid-xxx" })
→ ✅ Editado: src/app.js
```
---
## Audit Log Format
Each line is a JSON object:
```json
{"ts":"2026-08-29T21:30:00.000Z","action":"edit","target":"src/app.js","approved":true,"violations":0}
{"ts":"2026-08-29T21:31:00.000Z","action":"bash","target":"rm -rf /","approved":false,"violations":1}
```
---
## Compatibility
| IDE/Tool | Compatible | Notes |
|---|---|---|
| **opencode** | ✅ Native | `question()` is built-in |
| **Cursor** | ⚠️ Partial | MCP supported; `question()` depends on implementation |
| **Windsurf** | ✅ Likely | Active MCP support |
| **VS Code Copilot** | ⚠️ Partial | Growing MCP support |
| **Custom agents** | ✅ Adaptable | Replace `question()` with any human-input mechanism |
> If your IDE doesn't have `question()`, the LLM can still ask in plain text — but there's no UI enforcement.
> Set `HITL_QUESTION_TOOL` to match your IDE's tool name.
---
## System Prompt Integration
For maximum HITL enforcement, load `docs/hitl_protocol.md` as the **last** system instruction in your IDE config.
> Tokens at the **end** of the context receive more attention from transformers (recency bias).
> Loading HITL rules last maximizes compliance.
See `docs/llm_enforcement.md` for the full guide on writing effective HITL system prompts.
---
## License
MIT
TDQS
Scored across 4 tools
Each tool targets a distinct action (declare intent, edit, write, bash), but edit_hitl and write_hitl both handle file changes and the relationship between declare_intent_hitl's sessionToken and the per-action sessionToken is not fully clear. Overall, agents should be able to select the right tool with the guidance provided.
All tools share the _hitl suffix and follow a verb prefix pattern. declare_intent_hitl uses a compound verb while the others use single verbs, creating a slight inconsistency, but the pattern remains predictable and readable.
Four tools is well-scoped for a proxy server that adds human-in-the-loop approval to common operations. Each tool has a clear role and no redundant entries inflate the surface.
The server covers the core file edit/write and shell execution actions needed for an approval proxy, plus an explicit intent declaration step. Missing session audit/cancel tools are minor gaps that don't block the primary workflow.