npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mcp-gatekeeper

v1.0.1

Published

A PreToolUse gate for headless coding agents: static policy layers, protected-path rules and a local LLM judge, installed as a native hook into codex and antigravity.

Readme

mcp-gatekeeper

A policy gate for headless coding agents, installed as a native PreToolUse hook.

It sits in front of the agent's own tools — not in front of an MCP server — and classifies every file read, every edit and every shell command before it runs: hard credential boundaries, protected-path rules, per-segment shell decomposition, an audit log, and a local LLM judge for the minority of calls that static rules cannot settle.

npx mcp-gatekeeper install codex      # or: agy, or: all
npx mcp-gatekeeper status

Supports codex-cli and Antigravity CLI (agy). Install is user-level.

Name note. This package used to be an MCP proxy, and the name is kept for continuity. It no longer serves MCP at all — install removes the old gatekeeper MCP server if it finds one.

Why a hook instead of a proxy

The proxy worked, but only because every worker was prompted to abandon its native tools and route through it. That is a request, not a boundary — and a measured one: a codex worker once spent fourteen minutes editing a repo with its native tools and then reported the proxy "unavailable", while the proxy was running as its own child process the whole time.

A PreToolUse hook has no such gap. It is on the native tools, so there is nothing to be talked out of using, nothing to drown out with 122 competing tools, and no preamble asking the model to please cooperate. It also sees strictly more: on agy the hook fires for call_mcp_tool too, which the proxy could never observe.

What it actually does

agent (codex / agy)
  │  about to call a tool
  ▼
PreToolUse hook  ──►  hook.sh  ──►  hook.js
  │  0. tool circuit breakers  anything that spawns a second agent — final
  │  1. hard boundaries        deny-path patterns (credentials, this gate's own config) — final
  │  2. review-required        protected-path writes, critical-path rm/rmdir. Computed BEFORE
  │                            rules, because an allow rule must not pre-approve either.
  │  2b. read-only tier        MCP_GATEKEEPER_TIER=investigate refuses every mutation
  │  3. policy rules           deny > ask > allow, per whole call AND per shell segment
  │  4. obvious-safe           reads, and writes INSIDE the working directory        (~1 ms)
  │  5. verdict cache          dev loops are repetitive                              (~1 ms)
  │  6. judge                  everything else — typically a small minority of calls
  ▼
allow / deny   (never `ask` — see below)

Two properties worth calling out:

  • Compound commands are decomposed. git status && rm -rf / is refused: every segment must clear independently, including segments after || and ; and inside $(…).
  • A multi-file edit is judged per file. codex's apply_patch carries its own patch format; the gate parses out every Update File: / Add File: / Delete File: target and takes the most restrictive verdict. One protected path in a ten-file patch refuses the patch.

Speed

A process spawn sits in front of every tool call, so this matters more than it would anywhere else. Measured on an M5 Pro, through the installed wrapper:

| Path | Time | |---|---| | read-only shell, reads, writes inside the working directory | ~1 ms decision, ~25 ms wall including node start | | cached verdict | ~1 ms | | judge (local Qwen3.5-9B via LM Studio) | ~700-900 ms |

The gate imports almost nothing at module scope; the judge and its HTTP client are imported lazily, and the verdict cache is only read when a judge call is actually about to happen. The static layers settle the overwhelming majority of calls, which is what keeps the average near the top row. npx never appears in the hook commandinstall stages a copy of the runtime under ~/.config/mcp-gatekeeper/hook/ and bakes in an absolute node path, because paying package resolution per tool call would cost more than the gate itself.

Failing closed

This is the one property the proxy had for free and a hook does not. Measured 2026-09-12:

| Gate malfunction | codex 0.153.4 | agy 1.1.27 | |---|---|---| | explicit deny | blocked | blocked | | crash (exit 1) | ran | blocked | | invalid JSON | ran | blocked | | timeout / killed | ran | blocked | | empty stdout | ran | ran | | hook not trust-hashed | ran, with no warning at all | n/a |

So the gate cannot rely on the engine to fail safe. Three layers close it:

  1. hook.js wraps everything in try/catch and installs uncaughtException / unhandledRejection handlers that emit a deny in the engine's own wire format.
  2. An internal deadline (deadlineMs, default 20 s) fires well under the engine's hook timeout (install writes 60 s), so a hung judge produces a deny rather than a kill.
  3. hook.sh — written by install — substitutes a deny if the gate exits non-zero, prints nothing, or prints something that is not a JSON object. This is the only layer that survives node itself going missing, which is why the engines are pointed at a wrapper.

npx mcp-gatekeeper status runs the whole chain end to end and reports the verdict and latency.

ask is resolved here, never on the wire

On both engines, a hook ask is treated as approval. codex exec runs with approval policy never, so the command simply runs; agy does the same, verified in the real driver shape (--project with minted grants, no --dangerously-skip-permissions). Forwarding an ask is therefore forwarding an allow, which would silently permit every ask rule (git push --force, npm publish) and the entire 55-84 judge severity band.

So the gate resolves ask itself, per askPolicy (default deny). The one exception is an interactive codex session, which really can prompt and which the payload identifies via permission_mode. agy's payload carries no equivalent field, so agy asks are always resolved — if that refuses something you want in your own interactive sessions, add an allow rule to ~/.config/mcp-gatekeeper/config.json rather than setting askPolicy: "allow" wholesale.

A judge allow over a review-required call is floored at ask for the same reason: a protected-path write is meant to reach a human, not a classifier. This is not theoretical — a local judge scored apply_patch adding .git/config at severity 0 ("safe write to local git config"), and .git/config can set core.fsmonitor, which is arbitrary command execution on the next git invocation.

Install

npx mcp-gatekeeper install codex
npx mcp-gatekeeper install agy
npx mcp-gatekeeper install all
npx mcp-gatekeeper uninstall all

User-level, and it writes exactly this:

| Path | What | |---|---| | ~/.codex/config.toml | a marked [[hooks.PreToolUse]] block, appended | | ~/.gemini/config/hooks.json | an "mcp-gatekeeper" entry alongside any others | | ~/.config/mcp-gatekeeper/hook/ | the staged runtime | | ~/.config/mcp-gatekeeper/hook.sh | the fail-closed wrapper |

Both engine config files are copied to <file>.bak-mcp-gatekeeper before any change, and uninstall removes only what carries the mcp-gatekeeper marker — it will not disturb other hooks, and for codex it also matches by command string in case codex's own serializer has rewritten the file and dropped the marker comments.

Two engine quirks the installer works around, both measured:

  • codex silently skips an untrusted hook. No warning, no hook line — the call just runs ungated. Headless runs must pass --dangerously-bypass-hook-trust; an interactive codex prompts you to trust it once. Despite the flag's name, passing it is the safe option here.
  • agy ignores workspace-local .agents/hooks.json. Only ~/.gemini/config/hooks.json loads. The upside is that a worker in a throwaway worktree cannot switch off its own gate; the cost is that the gate is machine-global for agy.

Working roots

Roots decide exactly one thing: whether the working-directory fast path applies. Every hard boundary — deny-paths, protected paths, critical removals, ask rules, the subagent breaker — is root-independent. Resolution order:

  1. $MCP_GATEKEEPER_ROOTS (colon-separated). A driver always knows where it spawned the engine.
  2. The payload: codex's cwd, agy's workspacePaths.
  3. The nearest enclosing git repository of the call's own target path, stopping at $HOME.
  4. fallbackRoots in config.
  5. Nothing — degraded, not refused: writes and outside-reads simply go to the judge.

Step 3 exists because agy sends workspacePaths: [] in -p mode and overwrites both cwd and $PWD for the hook process, so for an ad-hoc agy session there is otherwise no way to recover the project directory at all.

Environment

| Variable | Effect | |---|---| | MCP_GATEKEEPER_TIER=investigate | refuse every mutation for this run — reads and read-only shell only | | MCP_GATEKEEPER_ROOTS | colon-separated absolute working roots | | MCP_GATEKEEPER_AUDIT | write this run's decision log here (a driver can then verify the gate actually ran — an empty file means it did not) | | MCP_GATEKEEPER_CONFIG | config JSON path | | MCP_GATEKEEPER_DEBUG_PAYLOAD | append every raw hook payload here, for diagnosing engine changes | | MCP_GATEKEEPER_JUDGE_API_KEY | for a hosted judge |

investigate is worth a note: it classifies each call rather than confining the process, which makes it strictly stronger than the OS sandboxes it replaces — those confine writes but do not confine reads at all.

Judge strategies

Same yes/no outcome, different substrate.

| Strategy | Use it for | Notes | |---|---|---| | openai | LM Studio, Ollama, llama.cpp, vLLM, DeepSeek, OpenAI, OpenRouter, Groq | one adapter — they all speak /chat/completions, so only the base URL changes | | anthropic | metered Anthropic API key | ~$0.0006/verdict on a small model | | command | claude -p, codex exec, agy -p, any script | escalation tier only; a CLI spawn per verdict is slow | | none | policy-only gating | no LLM in the loop at all |

The judge is configured in ~/.config/mcp-gatekeeper/config.json:

{
  "judge": {
    "strategy": "openai",
    "endpoint": "http://localhost:1234/v1",
    "model": "",
    "reasoningEffort": "none",
    "timeoutMs": 12000
  },
  "askPolicy": "deny",
  "deadlineMs": 20000
}

An empty model auto-detects the first non-embedding model the endpoint offers. If the judge is unreachable the gate denies — it does not fall through.

Picking an endpoint, and the three numbers that must not cross

A judge call sits in front of a tool call, so judge latency IS agent latency. Two measurements, same model (qwen3.5:9b), same prompt, 2026-09-12:

| Host | Per verdict | Verdict quality | |---|---|---| | LM Studio, M5 Pro (local) | ~0.8 s | correct on every case | | Ollama, i7-9700 8-core / 12 GB (LAN) | ~20 s | also correct on every case |

The remote box was not wrong, it was slow — and slow in a specific place. At ~960 prompt tokens and ~25 completion tokens, generation is only ~4 s of that; the rest is prompt evaluation. Which means a faster model helps far less than a machine that can evaluate a prompt quickly. Put the judge on the same machine as the agent if you can.

reasoningEffort: "none" is load-bearing, not a tuning knob: with it, qwen3.5 emits 25 completion tokens; without it, 242 (898 characters of chain-of-thought), which took the same remote host from 19 s to 53 s. It is honoured by both LM Studio and Ollama.

Three numbers must stay ordered, smallest first:

judge.timeoutMs  <  deadlineMs  <  the engine's hook timeout
     12000            20000              60  (seconds)

Cross them and you get a fail-open: the gate is killed by the engine before it can answer, and on codex a killed hook means the call runs. You only set the first two — install derives the hook timeout from deadlineMs and writes it into the engine config, so after changing deadlineMs, re-run install. A ~20 s judge needs roughly {"judge":{"timeoutMs":45000},"deadlineMs":60000}, which install then turns into a 100 s hook timeout.

Configuration

Config is read from --config, $MCP_GATEKEEPER_CONFIG, or ~/.config/mcp-gatekeeper/config.json, and never from the workspace being operated on. A repository must not be able to widen the permissions of the agent editing it. ~/.config/mcp-gatekeeper/**, ~/.codex/config.toml and ~/.gemini/config/hooks.json are all in denyPaths for the same reason: a gated worker must not be able to edit its own gate.

npx mcp-gatekeeper print-config prints the effective config; npx mcp-gatekeeper selftest runs a fixed battery of decisions (reads, builds, protected paths, critical removals, credential reads, subagent spawns, remote-script-to-shell) and prints the verdict, layer and latency for each.

Verifying a run was actually gated

Point MCP_GATEKEEPER_AUDIT at a per-run file and check it afterwards. One JSON line per gated call:

{"ts":"…","tool":"apply_patch","args":{"engine":"codex","calls":2,"rootSource":"env"},
 "decision":{"action":"allow","kind":"working-dir","reason":"…"},"durationMs":1}

An empty or missing file means the hook never ran — which, on codex, is indistinguishable from a clean run unless you check. Compare the line count against the engine's own tool-call count.

License

MIT