
Product
Socket Threat Intel Now Shows Which Malware Campaigns Were Blocked or Detected
Socket Threat Intel now shows which malware campaigns were blocked or detected in your organization, with links to scans and Firewall events.
@itcustomsolution/approval-gate-mcp
Advanced tools
Fail-closed human approval gates for AI agent actions, as an MCP server. Default-deny, TTL expiry, append-only 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.consume_approval returning ok: true (once, for the exact approved action), or
executeApproved in trusted code. Pending, approved-but-not-consumed, denied, expired,
timeout, error: none of those is an authorization; all of them mean no.status: "approved" is informational. Status responses
carry authorizesExecution: false and an effectiveStatus (approved_unconsumed,
approved_expired, consumed, revoked, ...). Trusted code that performs the side effect
should call executeApproved (see Trusted execution boundary),
which consumes the approval itself. A consume result that arrived over MCP, from a file or from
an agent is never proof of approval, however well-formed.~/.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, if the request carried them, the same operation
name and the same canonical JSON payload), and only if the clock read after all of its
persistence is still at or before the request's expiry.Requires Node.js 22 or later (Node 18 and 20 are past end of life; the MCP SDK's @hono/node-server
dependency needs Node 20+).
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, optional operation and JSON payload to bind). Returns an id (and payloadDigest). |
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, oldest first, one page at a time (limit 1-100, default 50; pass nextCursor back as cursor). |
consume_approval | Call right before acting. Succeeds once, only if approved, unexpired, and action (plus operation/payload, if the request had them) matches exactly. Anything else: do not proceed. Its JSON result is not a transferable credential. |
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
approval-gate list approved --limit 20 --after a1b2c3d4e5f6 # page through history
approval-gate archive --older-than-days 30 # retention: move finished requests aside
show and the approve confirmation print the operation and the full canonical payload (escaped)
when a request binds one: that is exactly what an executor will be allowed to run.
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.
When your own code (not the agent) performs the side effect, let the gate consume the approval at that boundary and run exactly the approved payload:
import { executeApproved } from "@itcustomsolution/approval-gate-mcp/dist/store.js";
// id comes from the agent; the action, operation and payload are what your code is about to run.
await executeApproved(id, { action, operation: "smtp.send", payload: { to, subject, body } }, async (ctx) => {
ctx.assertActive(); // re-check expiry immediately before the side effect
await smtp.send(ctx.payload); // ctx.payload is a frozen copy of what the human approved
});
executeApproved accepts no receipt. It performs the single-use consume itself (binding to the
action text, and to the operation and payload if the request carried them), writes an
execution_started audit line (fsync), checks expiry again, and only then calls your function. If
the start record cannot be written, or the approval expired during that write, nothing runs. Call
ctx.assertActive() again after any await that precedes the side effect.
assertConsumed(result, id, action, binding?) remains for code that calls consumeApproval
itself: it accepts only a grant object issued by consumeApproval in the same process, once,
for the same id/action/binding, before expiry. A copy, a JSON round trip (an MCP response, a file,
an agent message) or a hand-built object is always refused. 0.3.0 accepted any well-shaped object;
see the changelog.
Crash and replay semantics. Consumption is at-most-once: once .used is claimed the approval is
spent, whatever happens next, including refusals after the claim (expired during persistence, audit
failure). A crash after execution_started and before execution_finished means the outcome is
unknown: inspect the external system and, if needed, create and approve a new request. Nothing is
retried automatically, and there is no atomic guarantee between this gate's files and the external
system your callback touches. Details: docs/authorization-states.md.
| Limit | Value |
|---|---|
action, reason, decision reason | 8192 UTF-8 bytes each |
requested_by | 256 bytes (self-reported, never authenticated) |
operation | 1-128 of A-Z a-z 0-9 . _ : / - |
payload | JSON only; 32 KiB canonical (RFC 8785), depth 32, 10000 values; no "__proto__" key |
| TTL | MCP: integer 10 s to 7 days; library: any finite number up to 7 days (zero or negative gives an already-expired request) |
| Requests awaiting a decision | APPROVAL_GATE_MAX_PENDING, default 100, 1 to 10000. Concurrent creators can overshoot by at most their own number. Invalid values fail closed. |
| List page | 1-100 (default 50) |
| State directory | New requests are refused at 100,000 entries in requests/; listing refuses above 200,000. Reading, deciding, consuming and archive work at any size. |
When the queue is full, request_approval fails with approval queue full until a human decides
requests or they expire. Finished requests stay on disk until you run approval-gate archive, which
moves only finished requests that were created and expired at least 8 days ago, so nothing archived can
still be used. An MCP client can create and deny its own requests without a human, so it can fill
requests/ up to the creation limit; that blocks new requests (an availability limit, like the queue
cap), never a human's decisions. Each listing page reads every request file, so listing slows down as
history grows until you archive.
Everything lives in ~/.approval-gate/ (override with APPROVAL_GATE_DIR):
requests/*.json for current state, requests/*.final / *.used one-time decision and
consumption markers, audit.jsonl for the history and archive/ for archived requests. The
directory is created 0700 and files 0600.
Privacy: action text, reasons, requested_by and any bound payload are stored in plain text in
these files, and action text and reasons are copied into audit.jsonl. Do not put credentials or
secrets in them.
What the gate does protect against:
approve requires a TTY plus a typed confirmation).consume_approval is single-use and action-bound).\u escapes). Ordinary look-alike letters from other
scripts (a Cyrillic "а" in a hostname) are shown as they are: read request text, do not skim it.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.executeApproved and payload binding are local safeguards: they make the executed payload equal
the approved one when your trusted code uses them. They do not authenticate the human or stop a
same-user agent from bypassing the gate. A separately trusted authority/executor would still need:
None of that is created by this package; it needs an owner decision and new credentials. See docs/design-trusted-executor.md.
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
FAQs
Fail-closed human approval gates for AI agent actions, as an MCP server. Default-deny, TTL expiry, append-only audit log.
The npm package @itcustomsolution/approval-gate-mcp receives a total of 0 weekly downloads. As such, @itcustomsolution/approval-gate-mcp popularity was classified as not popular.
We found that @itcustomsolution/approval-gate-mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Product
Socket Threat Intel now shows which malware campaigns were blocked or detected in your organization, with links to scans and Firewall events.

Security News
Fixes for eMudhra and Brotli are out. Getting them everywhere they’re needed means tracking down bundled libraries, runtimes, and certificate stores.

Threat Research
/Security News
Socket uncovered secret-stealing workflows that ran in LiteLLM’s GitHub repository 11 days before the supply chain attack on PyPI and Docker.