Fail-closed human approval gates for AI agent actions. Default-deny, TTL expiry, audit log.
An MCP server that makes your AI agent stop and ask a human before it sends, deletes, publishes, pays or deploys.
Agents are good at doing things. The hard part is making sure the consequential things (sending an email, deleting data, publishing, spending money) only happen when a human said yes. This server is that gate, extracted from a system I have run in production for years: after an automation incident taught me the lesson the hard way, my standing rule became no automated destructive action without a human gate, ever. This is the generic, reusable version of that rule.
claude mcp add approval-gate -- npx -y @itcustomsolution/approval-gate-mcp
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"approval-gate": {
"command": "npx",
"args": ["-y", "@itcustomsolution/approval-gate-mcp"]
}
}
}
Approve or deny from your terminal with npx -p @itcustomsolution/approval-gate-mcp approval-gate list.
request_approval with
action: "send email to alice@example.com, subject: Invoice 1042" and gets back id a1b2c3d4e5f6.wait_for_approval. Nothing is sent.approval-gate list, then approval-gate approve a1b2c3d4e5f6 looks right.approved, calls consume_approval with the same id and the exact same action text (single use), and only then sends. Had you denied, ignored it, or let it expire, the answer is no.~/.approval-gate/audit.jsonl now has the request and your decision.approved.
Pending, denied, expired, timeout, error: all of those mean no.consume_approval authorizes. status: "approved" is informational. Status responses
carry authorizesExecution: false and an effectiveStatus (approved_unconsumed,
approved_expired, consumed, revoked, ...). Your trusted execution boundary must check a
consume result (assertConsumed(result, id, action) from the library) right before acting.~/.approval-gate/audit.jsonl (an append-only convention, not a tamper-proof log).consume_approval succeeds once, only for
the exact action text that was approved, and only before the request's expiry.From npm:
npm install -g @itcustomsolution/approval-gate-mcp # installs `approval-gate` and `approval-gate-mcp`
From source:
npm install
npm run build
npm link # makes `approval-gate` and `approval-gate-mcp` available on PATH
Claude Code:
claude mcp add approval-gate -- node /path/to/approval-gate-mcp/dist/index.js
Or in any MCP client config:
{
"mcpServers": {
"approval-gate": {
"command": "node",
"args": ["/path/to/approval-gate-mcp/dist/index.js"]
}
}
}
| Tool | Purpose |
|---|---|
request_approval | Register an intended action (action, reason, optional ttl_seconds). Returns an id. |
check_approval | Poll a request's status: pending / approved / denied / expired. |
wait_for_approval | Block up to timeout_seconds for a decision. Timeout = still pending = not approved. |
list_pending | List requests awaiting a human. |
consume_approval | Call right before acting. Succeeds once, only if approved, unexpired, and action matches the approved text exactly. Anything else: do not proceed. |
decide_approval | Deny a pending request, or revoke an approved one that has not been consumed. Approving through MCP is refused by default, so an agent cannot approve its own request; approvals come from the CLI. Operators can opt in with APPROVAL_GATE_ALLOW_MCP_APPROVE=1 (not recommended). |
approval-gate list # pending requests
approval-gate show a1b2c3d4e5f6
approval-gate approve a1b2c3d4e5f6 looks right, ship it
approval-gate deny a1b2c3d4e5f6 wrong recipient
approval-gate revoke a1b2c3d4e5f6 changed my mind # approved but not yet consumed
Put something like this in your agent's system prompt or policy file:
Before any outbound or destructive action (send, publish, delete, purchase, deploy), call
request_approvaland thenwait_for_approval. When it returnsapproved, callconsume_approvalwith the same id and exact action text, and proceed only if it returnsok: true. Never calldecide_approvalfor your own requests.
Everything lives in ~/.approval-gate/ (override with APPROVAL_GATE_DIR):
requests/*.json for current state, requests/*.final / *.used one-time decision and
consumption markers, and audit.jsonl for the history. The directory is created 0700 and
files 0600.
What the gate does protect against:
approve requires a TTY plus a typed confirmation).consume_approval is single-use and action-bound).What it does not protect against:
~/.approval-gate/, or simply skip the gate and perform the action. The gate is a cooperation
protocol plus an audit trail, not a sandbox. For hard enforcement, run the agent as a separate
OS user (or in a container) that cannot read or write the gate directory, and keep the gate's
CLI for yourself.consume_approval matches the action text. It cannot prove that the email,
SQL, or deploy the agent then runs equals what you read. That binding is cooperative. See
docs/design-trusted-executor.md for an enforced design (proposal only).audit.jsonl by anyone with write access to it.Setting APPROVAL_GATE_ALLOW_MCP_APPROVE=1 re-enables approval over MCP and removes the main
protection. Only use it when a human is the only one driving the MCP client.
State transitions, revocation, and crash behavior are specified in
docs/authorization-states.md. approval-gate revoke <id> (or deny
on an approved request) cancels an approval that has not been consumed yet.
Because the pattern matters more than the plumbing. In my production systems this same shape (request, out-of-band human decision, fail-closed default, audit trail) sits in front of email, publishing, and deployments, and it has removed entire classes of automation incidents. Start small, gate the scary things, and let your incident postmortems tell you what to gate next.
Issues and pull requests are welcome. Keep changes small and fail-closed: anything that
could turn a "no" into a "yes" needs a test. Run npm test before opening a PR.
See CHANGELOG.md for release history.
MIT, see LICENSE.
Built by Olufela "Lu" Fagbure · itcustomsolution.com · MIT license
Source-derived launch command. Check the maintainer’s required arguments and credentials before running:
npx -y @itcustomsolution/approval-gate-mcpMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-itcustomsolution-approval-gate-mcp": {
"command": "npx",
"args": [
"-y",
"@itcustomsolution/approval-gate-mcp"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.
Claude Desktop setup reference@itcustomsolution/approval-gate-mcpnpmio.github.itcustomsolution/approval-gate-mcp works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.