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

gatekeep-hook

v0.3.2

Published

PreToolUse adapter for Claude Code and OpenAI Codex CLI — routes tool calls through your gatekeep instance for authorization, approval, and audit.

Readme

Claude Code / Codex CLI hook adapter

One package, two runtimes: as of 0.3.0 this also governs OpenAI Codex CLI, whose PreToolUse hook framework was deliberately modeled on Claude Code's — see the Codex CLI section below. Everything in this document up to that section is Claude Code-specific and unchanged by that addition.

Makes gatekeep govern Claude Code itself, not just MCP agents. Claude Code's PreToolUse hook fires before any tool call executes — even under --dangerously-skip-permissions — and can return allow / deny / ask. gatekeep-hook.mjs is a thin adapter: read the tool call off stdin, POST it to gatekeep's real /api/mcp spine, translate the decision back to Claude Code's hook JSON.

Deliberately not using ask: a step-up (approval_required) is meant to be resolved by the agent's owner, from the gatekeep dashboard — not by whoever happens to be driving the session. Mapping it to Claude Code's local ask would just hand the decision back to that same person, defeating the point.

Instead the hook gives it ~9 seconds (three retries) to catch a near-simultaneous approval, then gives up and denies with a message telling the person exactly what to do: "Bash needs sign-off — open gatekeep → Approvals, approve or deny it, then ask me to try again." That's a deliberate design choice, not a shortcut: Claude Code hooks can't show live progress while still running (the CLI shows a fixed "Running PreToolUse hook" the whole time, and permissionDecisionReason only appears once the hook exits — confirmed against the hooks spec). A multi-minute silent poll would look identical to a hang. A quick, clearly-worded deny is more honest than a long wait with no visible explanation, at the cost of needing a manual retry after approving.

Claude Code's built-in tools (Bash, Edit, Read, ...) aren't behind a network MCP server, so they're registered under a Server with transport: "local". handleToolCall recognizes that and skips minting a credential / forwarding upstream — it only authorizes against grant_scopes and audits, then lets Claude Code execute the tool itself. Same spine, same audit_events table, same kill switch as every other agent.

Distribution

Shipped as a self-contained npm package (hooks/package.json) — zero third-party dependencies, invoked via npx gatekeep-hook. That was a deliberate choice over a compiled executable: anyone running Claude Code already has Node, so there's no runtime to install, and npm's own cache means there's no "what directory" decision to make on any device — that question just dissolves. A compiled binary would need a per-OS/arch build matrix to buy nothing, since the script has no dependencies to justify it.

Published to the real npm registry as gatekeep-hooknpx gatekeep-hook resolves it for real. (npm link inside hooks/ still works too, for local development against an unpublished change.)

Auth: real OAuth 2.1 access tokens, not the raw secret

As of 0.2.0, every call exchanges GATEKEEP_AGENT_SECRET for a short-lived (10 min), signed, audience-bound access token (POST /api/oauth/token) instead of presenting that secret directly — real OAuth 2.1 resource-server auth (src/lib/jwt.ts), not a bearer-string comparison. Since Claude Code spawns a fresh process per hook invocation (nothing persists in memory between tool calls), the minted token is cached in gatekeep-hook.token.json next to whichever config file is active, reused until ~60s before it expires, then re-minted transparently.

This is invisible day to day — same setup, same behavior — with two things worth knowing:

  • Add gatekeep-hook.token.json to .gitignore alongside the config file itself (holds a live, if short-lived, credential).
  • If token exchange fails for any reason (an older gatekeep instance without this endpoint yet, a transient network blip), the hook falls back to presenting the raw secret directly, exactly like 0.1.x always did — this is a graceful degradation between two valid presentations of the same credential, not a security downgrade, and it's why upgrading this package needs no corresponding change on the gatekeep side to keep working.

Setup (real device)

Register an agent in gatekeep first (/agents/new if you're an admin) and copy its secret — one agent per project you intend to scope this to, or one for the whole device if you're going global. Then, from the project you want governed:

npx gatekeep-hook init

It prompts for your gatekeep URL, the agent secret, who to attribute calls to, and finally where this should apply:

  • This project only — writes credentials to ./.claude/gatekeep-hook.env (this project's own file — a second project-scoped init elsewhere never touches it) and adds the hook to ./.claude/settings.json. Add .claude/gatekeep-hook.env and .claude/gatekeep-hook.token.json to .gitignore — the first holds a live secret, the second a minted access token.
  • Every project on this device — writes credentials to one shared per-OS path (%APPDATA%\gatekeep\hook.env on Windows, ~/Library/Application Support/gatekeep/hook.env on macOS, ~/.config/gatekeep/hook.env on Linux) and adds the hook to your global Claude Code settings (~/.claude/settings.json) — every session on the device picks it up.

Either way, init writes the actual .claude/settings.json entry for you — nothing to copy-paste. Every tool call now shows up in /activity on your gatekeep instance, and toggling a grant_scope (or hitting the agent kill switch) takes effect on the next tool call.

Config precedence when the hook actually runs: this project's own .claude/gatekeep-hook.env wins if present, then the device-wide global config, so a project-scoped init always takes priority over a global one for that project specifically.

Careful testing this on gatekeep's own dev session — wiring the hook into a project you're actively working in via Claude Code governs that same session's tool calls too. Give that agent a permissive starter policy before tightening it, or you'll end up approving your own assistant's every move mid-task (ask me how I know).

Local dev (working on this repo, before publishing)

npm run seed prints a "Claude Code (local)" agent secret and seeds a matching grant. Put it in hooks/.env (copy hooks/.env.example) rather than running init — that's a third, lowest-priority config location reserved for developing the hook script itself, separate from either a real project-scoped or global install. npm link inside hooks/ then lets .claude/settings.json use the plain gatekeep-hook command instead of a node /absolute/path reference, exactly like a real install would.

Demo

  • Read / Edit / Write / Glob / Grep — silent allow.
  • Bash / WebFetch — step-up: after ~9s Claude Code shows a denial explaining it needs sign-off in gatekeep's /approvals. Approve it there, then ask Claude Code to retry the same action — this time it goes straight through (the hook finds the now-approved row on its very first check).
  • Task (spawning a sub-agent) — blocked outright, no wait, same shape as the delete_files demo.
  • Suspend the "Claude Code (local)" agent from /agents → every subsequent tool call is denied instantly, mid-session.

The honest limit

This governs one Claude Code session that has the hook wired in. A session with the hook removed from its local settings bypasses gatekeep entirely — closing that gap needs managed (org-enforced) settings distribution, which is a deployment concern, not something this adapter can fix on its own.

Codex CLI

npx gatekeep-hook init --codex wires the identical decision logic into OpenAI Codex CLI instead of Claude Code. This works because Codex's hooks framework mirrors Claude Code's closely enough that no fork was needed: same tool_name/tool_input stdin fields, same hookSpecificOutput.permissionDecision output shape. Full research notes: docs/codex-adapter-notes.md.

Setup is otherwise the same flow as above — gatekeep URL, agent secret, attribution, then project-vs-device scope for credentials. The hook registration itself always goes to ~/.codex/hooks.json (user-level), independent of that scope choice; project-level Codex hook config is possible in principle but not what init writes today.

Two things to know before you rely on this:

  1. Trust review. Codex requires reviewing and trusting a new hook before it will actually run — init prints this, but it's easy to miss. Start a Codex session and run /hooks to trust it. Until you do, Codex silently skips the hook (with a warning) and every tool call runs completely ungoverned — the opposite of fail-closed.
  2. Coverage gap: hooks only fire for Bash. openai/codex#16732 is open as of this writing: ApplyPatchHandler never emits a hook payload at all, and hook_runtime.rs hardcodes tool_name: "Bash" regardless of what actually ran. In practice this means gatekeep currently governs shell/Bash commands only under Codex — file edits via apply_patch and MCP tool calls are not intercepted, allowed or denied, until upstream fixes this. Don't represent Codex coverage as equivalent to Claude Code's until it's fixed.

Both of those were known from reading Codex's docs (Phase 0). Live testing against a real, properly trusted codex session (Phase 2) surfaced four more, all fixed — see docs/codex-adapter-notes.md for the full detail: credential routing wasn't runtime-aware (a project's Claude Code config could silently shadow Codex's), a tool-name collision meant Codex's Bash calls resolved to Claude Code's Bash Tool row instead of registering its own, the 15s hook timeout was too tight for the existing approval grace-retry period, and Codex's "allow" response has a stricter schema than its docs' own example implies (it needs updatedInput, not permissionDecisionReason). None of these were guessable from the docs alone — this whole section only exists because of the live verification pass, which is exactly why Phase 2 wasn't skipped.

Why approval_required works the same way here

Codex's hook contract has no usable "ask" — it's documented as parsed but unsupported, and using it marks the hook run failed while letting the tool call proceed anyway (fails open). That sounds like it would force a different design for Codex, but it doesn't: this adapter never used Claude Code's ask either, for the same reason stated above (centrally-owned approval shouldn't be handed back to whoever's driving the local session). The existing grace-retry-then-deny behavior — retry ~9s, then deny with an actionable message — is reused unchanged for Codex, and happens to also be the only sound choice given Codex's fail-open "ask". Confirmed working live: a novel command blocks and does not run; approving it in gatekeep and retrying runs it for real.