Skip to main content
Glama

GATEKEEPER MCP

Stops coding agents from making changes they cannot justify.

CI npm version license: MIT

A local Model Context Protocol server that sits between a coding agent and any code-modifying action. Before the agent edits a file, it calls pre_action_check; if the change is not justified by evidence, or the task is already solved, the server tells the agent to stop and report back instead of writing code. Local-only: the server itself makes no network calls and sends no telemetry.

Install

Requires Node.js >= 20.

The two paths below need the package on npm. v1.0.0 is tagged but not yet published, so npx -y gatekeeper-mcp and the installer currently return a 404. They are the intended day-to-day install, and they are verified against a locally packed tarball rather than the registry. Until publication, use the from-source path at the bottom of this section.

Try it without installing. Let your agent fetch and run the package:

opencode mcp add gatekeeper -- npx -y gatekeeper-mcp

Install it globally. The installer places the binary in $HOME/.local and then prints the exact command for your agent:

curl -fsSL https://raw.githubusercontent.com/blxnkl1/GATEKEEPER-MCP/main/scripts/install.sh | bash

Options: --version <tag> to pin, --prefix <dir> to change the install prefix (default $HOME/.local), --dry-run to see what it would do. To remove it:

curl -fsSL https://raw.githubusercontent.com/blxnkl1/GATEKEEPER-MCP/main/scripts/uninstall.sh | bash

Work on the project, or install from source. This works today, with no registry access:

git clone https://github.com/blxnkl1/GATEKEEPER-MCP.git
cd GATEKEEPER-MCP
npm ci
npm run build
opencode mcp add gatekeeper -- node "$PWD/dist/index.js"

To put that local build on your PATH instead of pointing at a checkout:

npm install -g --prefix "$HOME/.local" .
"$HOME/.local/bin/gatekeeper-mcp"    # starts, logs to stderr, waits on stdio

Related MCP server: Sensory-Grounding MCP

Compatibility matrix

Agent

Status

Config path

Guide

OpenCode v1

UNVERIFIED

opencode.json

docs/agents/opencode.md

OpenCode v2

VERIFIED

opencode.json

docs/agents/opencode.md

Codex

UNVERIFIED

~/.codex/config.toml (unconfirmed)

docs/agents/codex.md

Claude Code

VERIFIED

~/.claude.json / .mcp.json

docs/agents/claude-code.md

Hermes Agent

PARTIALLY VERIFIED

~/.hermes/config.yaml

docs/agents/hermes.md

Configs that are not yet fully verified are marked accordingly in each guide.

What is GATEKEEPER MCP?

GATEKEEPER MCP is a single-binary MCP server that gates file modifications behind an evidence check. It exposes two tools: pre_action_check, which returns one of three decisions — allow, deny, or request_info — together with a reason and concrete next steps, and verify_decision, which checks that the decision an agent is about to report is the one the server actually issued. The agent is expected to call the first before every edit and to treat a deny as final. It runs entirely on your machine over STDIO, and sends no telemetry.

allow is issued only when all four of these hold, every one observed by the server rather than asserted by the agent:

  1. the reproduction command was executed and exited non-zero;

  2. the git working tree is verified clean;

  3. the project's own verification command was run;

  4. that verification command also failed.

The last two are the point. The agent chooses the reproduction, so a non-zero exit on its own proves only that the agent's command failed — {"command":"false"} is the cheapest way to produce one. Requiring the project's own tests to fail as well is what makes the claim falsifiable by something the agent does not control. A command that exits 0 can never reach allow, and neither can persuasive prose. See ADR 0007.

What allow does not mean. It means this project is verifiably failing, the tree is clean, and you supplied a command that fails. It does not mean the server has confirmed that your specific command demonstrates your specific bug. That distinction is the honest limit of the design and is documented rather than papered over.

Why?

LLM coding agents suffer from Action Bias: the tendency to change code when no change is warranted. A plausible-looking diff gets produced whether or not a bug existed, whether or not the code was already correct, and whether or not the agent can reproduce the problem it claims to be fixing. The result is churn that looks like progress and quietly breaks working software.

Estimates of the rate at which coding agents propose unnecessary edits commonly land in the 35–65% range. Treat that as an order-of-magnitude figure rather than a precise measurement: it varies wildly by task, model, and how "unnecessary" is defined, and the primary sources are informal. It is included here to establish that the problem is large, not to be cited as a hard number.

The intervention is deliberately blunt. Do not make an edit until you can show the problem it fixes.

Free and local, permanently

GATEKEEPER MCP runs entirely on your machine. GATEKEEPER itself performs no network egress — it opens no socket, resolves no name, sends no telemetry, and requires no API key or account. All runtime dependencies are free and open-source. The project is MIT-licensed and will remain so. See ADR 0005.

One qualification, because the broader claim would be false: the reproduction command GATEKEEPER runs on the agent's behalf can open network connections, the same way any command you run can. Network fetchers (curl, wget, nc, ssh) are on the default denylist to discourage that, but a permitted interpreter such as node -e can still reach the network. The guarantee is about the server, not about the processes it is asked to start.

The only cost you may incur is from the LLM that your coding agent uses — GATEKEEPER itself never calls an LLM.

Quick start with any agent

Use <PATH_TO_REPO> for the absolute path to your checkout of this repo. MCP servers start with an unspecified working directory, so a relative path will not resolve. If you installed from npm you can use npx -y gatekeeper-mcp instead and skip this section; see Install.

OpenCode — opencode.json:

opencode mcp add gatekeeper -- node <PATH_TO_REPO>/dist/index.js
{
  "mcp": {
    "servers": {
      "gatekeeper": {
        "type": "local",
        "command": ["node", "<PATH_TO_REPO>/dist/index.js"]
      }
    }
  }
}

Codex — ~/.codex/config.toml:

[mcp_servers.gatekeeper]
command = "node"
args = ["<PATH_TO_REPO>/dist/index.js"]

Claude Code:

claude mcp add gatekeeper -- node <PATH_TO_REPO>/dist/index.js

Hermes Agent — ~/.hermes/config.yaml. This command is interactive: it connects, lists the tools it finds, and asks which to enable. Answer Y.

hermes mcp add gatekeeper --command node --args <PATH_TO_REPO>/dist/index.js

Every command above works with npx -y gatekeeper-mcp instead of a local path.

The snippets above are a starting point. Prefer each agent's mcp add command over hand-editing a file: the CLI writes whatever the installed version actually expects, which is the one thing that changes between major versions.

Tools

pre_action_check

Gatekeeper check. Call this BEFORE modifying any code. It verifies whether the change is justified by evidence and whether the task is already solved. If not justified, the agent MUST NOT edit files.

Input

Field

Type

Required

Description

taskDescription

string

yes

What the agent is trying to accomplish.

proposedChange

string

yes

The diff, or a natural-language description of the change.

affectedFiles

string[]

no

Files the agent intends to modify.

evidenceOfProblem

string

no

Reproduction steps, a failing test, or an error log.

reproduction

object

no

An executable command that reproduces the problem.

reproduction is the field that matters. Free text is never accepted as proof, so evidenceOfProblem on its own can only ever produce request_info.

runTests was removed in v1.0.0. It used to let the agent decide whether the gate cross-checked its claim, which combined with the agent also choosing the reproduction command to make allow free. The verification command is now run by the server whenever it can change the verdict. Agents still sending runTests are unaffected: the field is ignored, not rejected.

reproduction field

Type

Default

Description

command

string

—

Bare executable, e.g. "npm", "pytest", "node".

args

string[]

[]

Arguments. Never put arguments in command.

cwd

string

repo root

Must resolve inside the repository root.

timeoutMs

number

30000

Kill after this long. Range 1000-300000.

expectFailure

boolean

true

Descriptive only. Records whether you expected this command to fail today. It cannot change the verdict: only a non-zero exit is ever treated as failure evidence.

The command is spawned directly, never through a shell. command: "npm test" is rejected, because accepting a string like that would mean handing it to a shell. Split it into command: "npm" and args: ["test"].

Output

Field

Type

Description

decision

"allow" | "deny" | "request_info"

The verdict.

reason

string

Why the verdict was reached.

nextSteps

string[]

Ordered instructions the agent should follow.

evidence

object

What the reproduction stage found.

state

object

What the state inspection stage found.

evidence field

Type

Description

state

"reproduced" | "not_reproduced" | "unverifiable" | "timeout"

Outcome of the reproduction.

detail

string

Human-readable explanation.

exitCode

number

Present only when a command actually ran.

durationMs

number

How long the command took.

state field

Type

Description

workingTree

"clean" | "dirty" | "unknown"

Git working tree status.

tests

"pass" | "fail" | "skipped" | "unknown"

Result of the test suite.

detail

string

Human-readable explanation.

Decisions

  • allow — justified, proceed with the edit. Reachable only when the reproduction exited non-zero, the working tree is clean, and the project's own verification command was run and also failed. A command that exited 0 cannot produce it. A project with no discoverable verification command can never be approved; set testCommand (see Configuration).

  • deny — not justified, do not touch any file. The agent should reply "No change needed — the current code already satisfies the task."

  • request_info — evidence is missing, ask the user before editing.

Reproduction command

A full call that can reach allow, for a bug where a test times out:

{
  "taskDescription": "Uploads hang when the network is slow",
  "proposedChange": "Add a bounded retry around the upload call in src/upload.ts",
  "affectedFiles": ["src/upload.ts"],
  "reproduction": {
    "command": "npm",
    "args": ["test", "--", "upload.test.ts"],
    "timeoutMs": 60000
  }
}

The server runs npm test -- upload.test.ts, reads the exit code, and returns:

{
  "decision": "allow",
  "reason": "Failure reproduced under a clean working tree. The change is justified.",
  "nextSteps": [
    "Make the minimal change that makes the reproduction pass.",
    "Do not refactor unrelated code."
  ],
  "evidence": {
    "state": "reproduced",
    "detail": "Reproduction succeeded: \"npm\" exited 1 in 843ms. Output: ...",
    "exitCode": 1,
    "durationMs": 843
  },
  "state": {
    "workingTree": "clean",
    "tests": "skipped",
    "detail": "The git working tree is clean."
  }
}

expectFailure records what you expected; it never decides anything. Set it to false when you are checking an assertion that is expected to pass today, such as one your change would break — the detail string will then note the expectation, so a reader can see the result contradicted it.

The verdict depends on the exit code alone. A command that exits 0 is a deny: the reproduction succeeded, so the code may already be correct. A command that exits non-zero is failure evidence even if you declared you expected it to pass, because a failure really was observed. That asymmetry is the point, and it is why a caller cannot pick a polarity that turns a successful command into an approval.

Trust and audit

Every pre_action_check reply carries a nonce, a random 22-character token. Before reporting a decision, call verify_decision with that nonce and the decision you intend to report; it answers whether the server actually issued that decision. A mismatch is recorded locally as a suspected fabrication.

Two modes, set in .gatekeeperrc.json:

  • advisory (default) — verify_decision is available and mismatches are recorded, but nothing requires the agent to call it.

  • enforced — a new pre_action_check call is refused while a previous decision is unverified, with the reason "Previous decision not verified. Call verify_decision first."

Enforcement is limited to sequence violations. MCP cannot block file edits. The server can refuse to produce a new verdict until the last one was acknowledged; it cannot stop a write, because it has no channel to the filesystem the agent edits. See ADR 0006.

Audit log. Set auditLog.enabled to record every decision, verification, and suspected fabrication as one JSON line. It defaults to ~/.gatekeeper/audit.log, rotates at 10 MB keeping one generation, and stores taskDescription as a truncated SHA-256 rather than as text. It is off unless you turn it on, and it never leaves the machine.

Configuration is read from $GATEKEEPER_CONFIG, then .gatekeeperrc.json in the working directory, then ~/.gatekeeperrc.json. An invalid file stops the server rather than silently reverting to defaults.

{
  "mode": "advisory",
  "rateLimit": { "windowMs": 60000, "max": 20 },
  "denylist": {
    "commands": ["rm", "dd", "mkfs", "shutdown", "reboot", "halt"],
    "patterns": ["rm\\s+-rf\\s+/", "curl.*\\|.*sh"]
  },
  "auditLog": { "enabled": true, "path": "~/.gatekeeper/audit.log" },
  "nonceStore": { "maxSize": 1000, "ttlMs": 300000 },
  "testCommand": ["pytest", "-q"],
  "testTimeoutMs": 180000
}

testCommand is the project's verification command as an argv array, not a string, so declaring it never introduces a shell. It is operator configuration and never agent input: this is the command whose failure makes allow possible, so the agent must not be able to choose it. When it is unset, the command is detected from the lockfile and package.json scripts.test as before.

Set it if your project is not npm-based, or if you want a narrower command than the whole suite. A project with no discoverable verification command can never be approved, because the gate would have nothing to cross-check the agent's claim against. testTimeoutMs bounds that command; the default is 120000 ms.

No configuration can switch the security invariant off. mode, rateLimit, denylist, nonceStore and auditLog change pressure, observability and resource bounds; none of them can turn a zero exit or an unverified project into an allow.

Rate limit. 20 calls per 60 s by default, per session. Exceeding it returns a deny rather than a tool error, because being throttled is a decision the gate reached, not a failure.

Command denylist. Reproduction commands whose basename is listed, or whose command line matches a listed pattern, are refused before anything is spawned. The default list includes the shells and script hosts (sh, bash, zsh, env, python3, perl, ruby, …) as well as destructive commands and network fetchers, because bash -c "…" is a shell by another name.

The denylist is a refusal list, not a sandbox and not the control that protects the invariant. node stays allowed because it is ordinary tooling, and node -e is a whole interpreter in one argument. What makes allow unreachable for such a command is that the project's own verification command must fail too — see Execution boundary for the full list of what is and is not constrained.

Execution boundary

What this server does and does not constrain. The difference between a gate and a sandbox is the difference between a useful tool and a false claim, so it is worth being exact.

Constrained

  • the shape of the command it spawns: one primitive, shell: false, a bare executable plus an argv array, stdin closed, output capped at 16 KB per stream, time bounded and SIGKILLed on expiry;

  • the working directory: resolved through realpath and required to be the repository root or beneath it, so .., absolute paths, and symlinks pointing outside are all refused;

  • which commands are acceptable, via a denylist you can extend;

  • what the verdict may claim: every term in the allow conjunction is server-observed.

Not constrained

  • What the child process does. No seccomp, no namespace, no chroot, no resource limit beyond output length. A permitted command can read and write anything the user can.

  • Which files the child touches. Only cwd is confined; args are passed through verbatim. "Confined to the repository root" describes the working directory, not filesystem access.

  • Network egress by the child. The server itself opens no socket. A reproduction command can, which is why fetchers are denylisted — and why node -e still reaches the network.

  • Descendant processes. SIGKILL goes to the direct child only, so a reproduction that daemonises can outlive the call.

  • Whether a failure matches the bug you described. See ADR 0007.

  • Whether you edit a file after a deny. MCP is request/response over stdio; the server has no channel to the filesystem you write through. Enforcement is limited to sequence violations. Making a deny binding requires gating the write tool on the check result, which is agent configuration, outside this project.

Development

npm run dev        # watch mode via tsx
npm test           # vitest, single run
npm run test:watch # vitest, watch mode
npm run typecheck  # tsc --noEmit
npm run lint       # biome check
npm run format     # biome format --write
npm run build      # compile to dist/

Project layout

src/
  index.ts                  entrypoint: stdio transport
  server.ts                 McpServer factory and tool registration
  context.ts                server-scoped state shared by the tools
  logger.ts                 pino, bound to stderr
  tools/                    pre_action_check and verify_decision
  engine/                   analyzer + reproduction / state / decision stages
  schemas/                  zod input and output schemas, denylist
  config/                   .gatekeeperrc.json loader
  store/                    CSPRNG nonce store
  ratelimit/                sliding-window limiter
  audit/                    append-only local audit log
  types/                    domain types
tests/
  security/                 black-box ALLOW-invariant tests over real MCP
  guarantees/               mechanically enforced promises

stdout is reserved for the JSON-RPC frame stream. All logging goes to stderr. SKILL.md is the agent-facing contract and is the file to read if you are building an agent that must respect the gate.

Architecture: docs/architecture.md · Roadmap: docs/phases.md · Decisions: docs/decisions/

License

MIT — see LICENSE.

Available Tools

1 tool
pre_action_checkPre-action gatekeeper checkA
Read-onlyIdempotent

Gatekeeper check. Call this BEFORE modifying any code. It verifies whether the change is justified by evidence and whether the task is already solved. If not justified, the agent MUST NOT edit files. Supply an executable reproduction under reproduction (a bare command plus args, never a shell string): free-text evidenceOfProblem alone is never enough to obtain an "allow".

ParametersJSON Schema
NameRequiredDescriptionDefault
runTestsNoRun the project test suite as extra evidence. Default false.
reproductionNoAn executable command that reproduces the problem.
affectedFilesNoFiles the agent intends to modify.
proposedChangeYesThe diff, or a natural-language description, of the proposed change.
taskDescriptionYesWhat the agent is trying to accomplish.
evidenceOfProblemNoFree-text proof that a problem exists. Not executable on its own; supply `reproduction` to obtain an allow decision.

Output Schema

ParametersJSON Schema
NameRequiredDescription
stateYesWhat the state inspection stage found.
reasonYesOne or two sentences explaining the verdict.
decisionYesThe verdict for the proposed change.
evidenceYesWhat the reproduction stage found.
nextStepsYesOrdered, actionable instructions.

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and destructiveHint=false. The description adds valuable behavioral context: the tool acts as a gate that can block edits, verifies task completion, and mandates a specific reproduction format. It does not explicitly state the return decision structure, but the output schema likely covers that, and the 'allow' requirement implies it.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is concise—four sentences that front-load the main purpose and then detail the critical constraint. Every sentence earns its place: the gatekeeper role, the requirement to call before editing, the reproduction format, and the rule about evidence. No filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's moderate complexity (6 params, nested object, output schema), the description fully covers the essential usage: when to call, what to supply, and the decision rule. The output schema covers return values, so no additional return explanation is needed. The description is complete for an agent to correctly invoke the tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, baseline is 3. The description enriches parameter understanding beyond the schema by specifying that `command` must be a bare executable, `args` must not be embedded in `command`, and that `evidenceOfProblem` alone is insufficient without `reproduction`. This adds practical usage meaning to the parameters.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb ('verify'), the resource (whether a change is justified and task already solved), and context (before modifying code). It clearly distinguishes the tool's role as a gatekeeper without ambiguity, and no siblings exist to confuse.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly instructs 'Call this BEFORE modifying any code' and sets a hard rule: a reproduction is required to obtain an 'allow', and free-text evidence alone is insufficient. This gives clear when-to-use and what-to-provide guidance, with no alternative tools to compare.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.0.1
    • First observedpre_action_check

TDQS

A4.5/5.0

Scored across 1 tool

Disambiguation5/5

The server exposes exactly one tool, `pre_action_check`, so there is no possibility of confusing it with another tool. The purpose of the gate check is single and unambiguous.

Naming Consistency4/5

The single tool name `pre_action_check` is clear and snake_case, but it does not follow a verb-noun pattern and there is no set of tools to establish a broader naming convention.

Tool Count3/5

With only one tool, the server feels thin for the 'gatekeeper' domain, though the narrow purpose partially justifies the count. Auxiliary tools like policy lookup or reproduction validation are absent.

Completeness4/5

The core gatekeeping interaction is covered, but the surface lacks related operations such as querying gate rules or validating evidence formats, so coverage is functional but has minor gaps.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers