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

vinctor-codex-hook

v0.4.0

Published

Codex CLI PreToolUse hook routing tool calls through Vinctor runtime authorization.

Readme

Vinctor Codex CLI Hook

Status: Boundary Preview

CI

A Codex CLI PreToolUse hook that routes tool calls through runtime authorization before execution.

Why This Exists

AI agents are no longer just generating text. Across agent systems, they are increasingly executing tools — running shell commands, editing files, calling third-party services, triggering deployments, and reaching into systems that hold sensitive data. The surface area of "what an agent can touch" is expanding faster than the surface area of "what an agent has been explicitly authorized to do."

Static credentials and prompt-level safety guidelines do not cover this gap. An agent's allowed scope depends on the task, the operator, the target resource, and the moment. It changes during a session, can be revoked mid-run, and must be evaluated against operator-defined policy — not against the agent's own reasoning.

A runtime authorization boundary — one that decides, per tool call, whether a specific agent may perform a specific action on a specific resource right now — narrows that gap by making selected tool calls subject to an authorization decision before execution. This repository implements that boundary for Codex CLI.

What This Repository Contains

This repo holds the runtime boundary that selected Codex CLI tool calls pass through before they execute. It is one piece of the broader Vinctor runtime authorization infrastructure. The authorization service itself — grant model, policy evaluation, audit log, revocation — lives separately.

The hook does two things, in this order:

  1. Asks the authorization service for a decision. When Codex is about to invoke a configured tool, it runs the hook for the PreToolUse event, the hook maps the tool input to an (action, resource) pair, and then calls the authorization service for a permit-or-deny decision dynamically.

  2. Acts on the decision.

    • Service permit → the hook emits nothing and exits 0, so Codex proceeds.
    • Service deny, unreachable, timeout, malformed input, missing required env → the hook fails closed and returns deny with a fixed-template reason.
    • Tool call cannot be classified by config or built-in defaults → the hook abstains (emits no decision), deferring to Codex's own approval/sandbox flow.

    The hook does not include the underlying grant reference, audit event id, or matched scope in any reason string.

Codex CLI (PreToolUse event)
        │
        ▼
   This hook ── maps → asks the authorization service
        │                          │
        │                  permit / deny / fail-closed
        ▼                          │
   continue / deny  ◀──────────────────
        │   (or, if unclassifiable: emit nothing → Codex's native approval flow)
        ▼
Codex CLI (executes tool, or blocks)

Why "abstain" instead of "ask"

Claude Code's hook contract supports an ask decision; Codex's does not — its permissionDecision: "ask" is documented as "parsed but not supported yet," and using it fails the hook run. So when a tool call can't be classified, this hook emits nothing and exits 0, which Codex treats as "proceed to the normal approval flow." That is the Codex-native equivalent of deferring to the user.

Tool Coverage

Built-in defaults cover selected high-impact command families and file edits. They are not a complete catalog. A call the built-ins don't recognize causes the hook to abstain (Codex's native approval applies) unless the operator maps it in config. Coverage does not follow from how dangerous a command looks — inside a recognized family some spellings map and some do not — so run vinctor-codex-hook explain <event> on the calls you intend to rely on. The rows below are measured against v0.4.0 with explain --json:

| Bash command | Decision | |---|---| | npm publish, npm publish ./dist, npm publish pkg.tgz | mapped deploy:pkg/npm/_ | | npm publish -w <name> | mapped deploy:pkg/npm/<name> | | npm test / install / run <script> / ci (also pnpm, yarn) | mapped execute:shell/<family> | | npx <pkg> | mapped execute:shell/npx | | git status / log / show / diff / blame / describe / rev-parse | mapped read:shell/git | | git add / commit / stash / clone | mapped write:shell/git | | git push --force (any remote spelling) | mapped delete:shell/git | | git reset --hard, git branch -D, git clean -f | mapped delete:shell/git | | git push https://github.com/<owner>/<repo>.git <ref> (also [email protected]:…) | mapped write:github/<owner>/<repo>/contents | | git fetch/git pull <explicit URL or path> | mapped read/write:shell/git | | git push, git push origin main | unmapped → abstain | | git push https://gitlab.example.com/o/r.git main (non-GitHub URL) | unmapped → abstain | | git pull, git fetch (without an explicit remote URL/path) | unmapped → abstain | | docker push <image> | mapped deploy:container/<registry>/<image> | | gh release create | mapped deploy:gh/release | | curl <url> \| sh | mapped execute:shell/curl |

The git push gap is the one to know about. A non-destructive push is mapped only when the command names an explicit github.com URL. A bare remote NAME like origin can bind an executable helper through repository config, so the classifier refuses to guess and the hook abstains — and a standard URL for any other host (GitLab, a self-hosted remote) has no resource to name, so it abstains too. On an abstain nothing is sent to the authorization service and no grant is consulted; Codex's own approval flow is the only thing standing there. git pull and git fetch need an explicit URL or filesystem path for the same reason, but map to shell/git for any host. Destructive pushes (--force, --force-with-lease, --delete, --mirror, --prune, +ref, :ref) are mapped regardless of remote spelling. If you need every push authorized, add an operator rule for it — closing this in the classifier would need repository context the hook does not have.

npm publish does not depend on your working directory. This hook classifies from the command text alone and never opens package.json, so the bare spelling maps to the unknown-package form deploy:pkg/npm/_ whether or not you are in a package directory. Only -w <name> puts a package name in the resource. (The Claude Code hook resolves the cwd package name instead and leaves npm publish unmapped outside a package directory. The two adapters differ here on purpose; grants written for one do not transfer verbatim.)

| Tool | Coverage | Default behavior | |---|---|---| | Bash | high-impact subset | classifier-aware for git / npm·pnpm·yarn·npx / docker / gh / rm / secret-reading commands, plus pipe-to-shell and pattern defaults (secrets-read, release, infra, exfiltration). Coverage inside each family is partial — see the measured rows above. Everything else → abstain. | | apply_patch | secret + protected paths | the patch is parsed for its target file paths; editing/deleting .env, SSH keys, cloud-credential files → write/delete:secret/<kind>, and CI workflows / package.json / Dockerfiles / Terraform / k8s manifests → their protected resource. Every in-boundary target is authorized separately and the patch proceeds only if all of them permit. Ordinary file edits → abstain. | | Read / Write / Edit / MultiEdit † | secret + protected paths | by file_path: reading and writing .env, SSH keys, and cloud-credential files is in-boundary (read/write:secret/<kind>); writing CI workflows / package.json / Dockerfiles / Terraform / k8s manifests is write:<resource>. Ordinary files → abstain. | | WebFetch † | full (classification) | every fetch maps to send:net/internal/<host> or send:net/external/<host>. Operator config can override per host. | | WebSearch † | matcher only | no built-in mapping; operator config required, otherwise abstain. | | mcp__<server>__<tool> | built-in classifiers for filesystem, github, slack; other servers: matcher only | These three servers' common tools are mapped out of the box. Tools they don't recognize, and all other MCP servers, require operator config — otherwise abstain. |

Compound operations are authorized on every effect they cause (PKA-145). An mcp__filesystem__move_file causes three: the destination gains state (write), and the source is disclosed at a new location (read) and removed (delete). The hook makes one /v1/enforce call per distinct pair and permits the call only if all of them permit; the first refusal is a deny. It used to send the destination write alone, so a destination-write grant moved a protected file out of its subtree with no read and no delete on the source. The same rule governs apply_patch: every in-boundary target in the patch is a separate check. Single-effect operations are unchanged and still make exactly one call.

This holds on the operator-config path too: a rule may ADD a charge; it may never SUBTRACT an effect. Its pair becomes the primary one, and the classifier's entire required set is required alongside it — so a rule naming move_file still requires the source read, the source delete AND the destination write. A rule cannot override a refusal either: when the classifier declines to map a multi-path call ("never move an ambiguous target"), config cannot turn that into a single-pair permit.

What counts as net/internal. WebFetch host classification treats these as internal: localhost, *.local, *.internal (the ICANN reserved private-use TLD), private/loopback/link-local IPv4 (10., 172.16–31., 192.168., 127., 169.254.), the unspecified/wildcard binds (0.0.0.0, 0.0.0.0/8, ::), the limited broadcast (255.255.255.255), IPv4 (224.0.0.0/4) and IPv6 (ff00::/8) multicast, and IPv6 loopback / ULA (fc00::/7) / link-local (fe80::/10). IPv4 addresses embedded in IPv6 (::ffff:127.0.0.1, ::169.254.169.254) are decoded and classified as the IPv4 address they name, and a trailing DNS root dot is canonicalised away (localhost. is localhost). Everything else is net/external. Operator config can override per host.

Limitation: classification is by NAME, not by resolved address. A hostname is classified from the string alone — the hook never resolves it. A public name that resolves (or is re-bound) to a loopback, private, link-local, or metadata address is still classified net/external, so a grant for send:net/external/* authorizes it. This is inherent to a PreToolUse boundary: the hook does not own the socket, so any address it resolved could differ from the one the runtime finally connects to (DNS rebinding), and resolving here would give false assurance rather than protection. Do not rely on net/external as an SSRF boundary against attacker-chosen hostnames. Where that guarantee is required, enforce at the resource side, where the connection is actually made. Literal-IP spellings — including the alternate encodings above — are fully classified.

Opt-in strict hostname policy (VINCTOR_NET_HOSTNAME_POLICY). Default name — unchanged behaviour, and the behaviour for an unset, empty, or unrecognised value, so a typo can never silently turn enforcement on or off. Set it to exactly strict to withhold the universal net/<scope>/<host> built-in for the one case this hook cannot account for: a host that is not a literal IP and classifies external — a public NAME whose resolved destination the hook never sees. Such a call is denied (hostname_not_literal), not deferred. Literal IP addresses in every spelling keep their mapping, because classification fully accounts for them; so do reserved internal names (localhost, *.local, *.internal, RFC 6761 .localhost), because they already classify internal and an external-only grant never covered them. An operator config rule naming a host still authorizes it under strict, since operator config is consulted before the built-in.

What strict does NOT do. It does not resolve DNS and it does not stop DNS rebinding. For a hostname an operator has explicitly allowed by rule, the destination is still whatever the resolver returns at connect time, which this hook never observes. strict narrows which names get a built-in mapping; it does not make net/external an SSRF boundary. That guarantee belongs at the resource side, where the connection is actually made.

† Codex hook-firing caveat — this matters. The table above describes what the hook classifies if it receives the event. It does not assert that Codex fires PreToolUse for every one of these tools. Codex's own docs call PreToolUse "a guardrail rather than a complete enforcement boundary": it fires for Bash and apply_patch, while broader shell interception and most MCP tool calls remain incomplete or version-dependent. Read / Write / Edit / MultiEdit / WebFetch / WebSearch are Claude Code tool names — whether your Codex build emits a hook event under those names is version-dependent and not guaranteed. They are supported here so the boundary is ready if/when the runtime surfaces them (and for non-Codex runtimes that reuse this binary), not because Codex is promised to fire them. This hook cannot make Codex fire a hook it doesn't fire. Verify on your installed version. This is not raw interception.

To measure your build: a reproducible harness lives in tools/codex-coverage/, and the per-tool coverage matrix (RUNTIME emitted? vs. MAPPING action:resource) plus a runbook is in docs/validation/coverage-probe/coverage-matrix.md. The matrix records measured Bash and apply_patch surfaces separately from unmeasured MCP and non-native tool names. Neither path ships in the npm tarball (it carries dist/, the plugin manifest and this README only), so both links point into the repository — clone it to run them.

What This Is Not

Vinctor authorizes configured, mediated tool calls routed through an adapter boundary. Unwrapped tool paths remain outside Vinctor's boundary. Vinctor does not provide OS/process/account isolation, sandboxing, raw tool interception, provider credential control, or rollback of already-started work.

This repository ships an independent Codex plugin; it is not an official OpenAI integration. It provides a PreToolUse boundary for the Vinctor service and applies only to the Codex hook paths you configure, and only where Codex fires the hook.

Install (Boundary Preview)

Before starting, have these ready:

  • Codex CLI installed (this is a Codex CLI hook).
  • A running Vinctor authorization service endpoint to point VINCTOR_ENDPOINT at — the hook asks it for every decision and does not run it for you. You can run one yourself in two commands: pip install vinctor-core then vinctor local start --db ./vinctor.sqlite (see below).
  • A valid agent grant: an agent key (aak_…) and a grant reference (grt_…) issued for your agent — vinctor local start issues both and prints them. Without them the hook fails closed (you can still evaluate mapping/abstain offline — see below).

Install from npm (recommended):

npm install -g vinctor-codex-hook
vinctor-codex-hook --version

Or from source (contributors):

git clone https://github.com/vinctor-ai/vinctor-codex-hook.git
cd vinctor-codex-hook
npm install
npm run build          # CLI at dist/src/cli.js

The repository root is a Codex plugin (.codex-plugin/plugin.json plus hooks/hooks.json). Install it through a trusted local marketplace, enable it, then review and trust its hook with /hooks. Codex skips new or changed non-managed hooks until their current definition is trusted.

For direct wiring, register the CLI in ~/.codex/hooks.json or a trusted project .codex/hooks.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash|apply_patch|Edit|Write|mcp__.*",
        "hooks": [{ "type": "command", "command": "vinctor-codex-hook" }]
      }
    ]
  }
}

If the harness session doesn't inherit your shell PATH, use the absolute bin path instead (command -v vinctor-codex-hook), or the built <absolute path>/dist/src/cli.js for a source install.

[!IMPORTANT] Verify the hook in /hooks after installing or changing it. A trusted plugin hook was observed receiving PreToolUse with tool_name: "Bash" on Codex 0.137.0, 0.139.0, and 0.144.1. Codex 0.144.1 also emitted apply_patch with the patch envelope in tool_input.command; a protected-path patch was mapped and blocked before the file was created. A second run with a mock service permit completed the hook and created the exact requested file. Coverage still varies by tool surface.

Keep the matcher scoped to Codex's documented blocking surfaces. Edit and Write are aliases for apply_patch; mcp__.* covers MCP calls. A tool call whose tool_name the hook doesn't recognize is treated as malformed input and denied (fail-closed) — so a broad matcher like .* would block every unrecognized or future Codex tool, not defer it. Match only the tools listed.

[!WARNING] Confirm the hook is actually firing before you rely on it. Until the hook is trusted in /hooks, Codex silently skips it — and headless codex exec does not print a "hooks need review" warning, so a mapped command can run with no hook: PreToolUse line and look as if Vinctor allowed it. After installing and trusting, run one mapped command in an interactive session and confirm you see hook: PreToolUse fire. The offline classifier is a quick pre-check that mapping works (it does not prove the hook is wired into Codex):

printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm publish"}}' \
  | vinctor-codex-hook explain /dev/stdin   # → mapped deploy:pkg/npm/_

Required env in the Codex session:

  • VINCTOR_ENDPOINT — base URL of the Vinctor authorization service
  • VINCTOR_AGENT_KEY — agent API key (aak_…)
  • VINCTOR_GRANT_REF — opaque grant reference (grt_…)

Optional:

  • VINCTOR_BOUNDARY_ID — boundary id from the local Vinctor service. It is required by fresh vinctor-core 0.6.0 databases; upgraded databases retain their previous mandate default. The hook sends it as X-Vinctor-Boundary-Id.
  • VINCTOR_CODEX_HOOK_CONFIG overrides the default config path .vinctor/codex-hook.json.
  • VINCTOR_HOOK_DEBUG=1 makes hook mode write one diagnostic line to stderr per event, naming the resolved config path and whether it was found. It never touches stdout (the hook decision) and never echoes secret env values. Leave it unset in normal use.

Unclassified calls are reported to /v1/observe before the hook abstains, so mapping gaps are visible centrally rather than only locally. Delivery is best-effort and bounded (500 ms): a failure does not change the abstain, but emits a fixed diagnostic to stderr, without tool input or credentials.

Known gap against vinctor-core 0.6.0. The current /v1/observe rejects any field beyond classification (HTTP 400), and this hook also sends outcome and tool_name — so every abstain currently prints vinctor-codex-hook: blocked-unmapped observation failed on stderr. The abstain itself is unaffected (stdout stays empty, exit 0) and Codex's approval flow proceeds normally; only the central visibility of that mapping gap is lost.

This is a Boundary Preview. Not production-ready. Not an official OpenAI integration.

Getting a grant, and evaluating offline

Real permit/deny decisions require a running Vinctor authorization service and a grant issued for your agent. The hook itself does not run that service and does not issue grants — it only classifies a tool call and asks the service.

  • VINCTOR_ENDPOINT, VINCTOR_AGENT_KEY (aak_…), VINCTOR_GRANT_REF (grt_…), and VINCTOR_BOUNDARY_ID come from the Vinctor authorization service, not from this repo.

  • Run that service locally. vinctor-core is on PyPI and its local start bootstraps a workspace, an agent key and a grant, then serves /v1/enforce until you stop it:

    pip install vinctor-core
    vinctor local start --db ./vinctor.sqlite \
      --scope 'deploy:pkg/npm/*' --scope 'write:shell/git'

    It prints shell export lines and keeps running:

    # Vinctor local service exports
    export VINCTOR_ENDPOINT="http://127.0.0.1:8765"
    export VINCTOR_AGENT_KEY="aak_…"
    export VINCTOR_GRANT_REF="grt_local"
    export VINCTOR_WORKSPACE_KEY="wsk_…"
    # Grant expires at <timestamp>.
    # Store these raw keys outside the repo; SQLite stores hashes only.
    # Local Vinctor service listening. Press Ctrl+C to stop.

    This hook reads only the first three. VINCTOR_WORKSPACE_KEY administers grants and must never be handed to the hook or the agent session. Export the three into the Codex session and the decisions above become real: with the two scopes shown, npm publish and git commit are permitted and git reset --hard comes back deny: action_denied. Repeat --scope per scope; the default is write:repo/feature/* only, so an unscoped local start denies most of the mapped table. Default port is 8765 (--port 0 for any free port). The grant is time-limited — re-run local start or raise --grant-ttl-hours when it expires.

  • Without those env vars you can still evaluate the hook offline, because its decision is observable without ever reaching the service:

    • A mapped call with missing/incomplete auth env returns deny: missing_auth_env — the success signal that the hook classified the call and would have asked the service.
    • An unmapped call emits nothing (abstain) — the hook could not classify it and defers to Codex.
    • A mapped call whose service is unreachable returns deny: service_unavailable — the hook fails closed.

So offline you can fully validate mapping, abstain, missing_auth_env, and fail-closed behavior; only the live permit / deny: action_denied outcomes need a running service with a valid grant.

Quickstart (end to end)

# 1. Install
npm install -g vinctor-codex-hook

# 2. (optional) Write an operator policy
mkdir -p .vinctor
cat > .vinctor/codex-hook.json <<'JSON'
{ "version": 1, "rules": [
  { "tool": "apply_patch", "matchType": "glob", "pattern": "**/migrations/**",
    "action": "deploy", "resource": "db/migration" }
] }
JSON

# 3. Validate the policy (offline; exit 0 valid, 1 invalid, 2 unreadable)
vinctor-codex-hook validate .vinctor/codex-hook.json

# 4. See how a specific call would map (offline; no service call)
printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm publish"}}' > /tmp/e.json
vinctor-codex-hook explain /tmp/e.json --json   # → mapped deploy:pkg/npm/_

# 5. Run one event through the hook itself (no auth env → fails closed at the boundary)
printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm publish"}}' \
  | vinctor-codex-hook          # → deny: missing_auth_env  (mapped, would ask the service)

# 6. Start a real authorization service and re-run step 5 against it
pip install vinctor-core
vinctor local start --db ./vinctor.sqlite --scope 'deploy:pkg/npm/*' &
#   → prints: export VINCTOR_ENDPOINT=... VINCTOR_AGENT_KEY=aak_... VINCTOR_GRANT_REF=grt_...
#   paste those three exports into this shell, then:
printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm publish"}}' \
  | vinctor-codex-hook          # → no output, exit 0  (permitted by the grant)
printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git reset --hard"}}' \
  | vinctor-codex-hook          # → deny: action_denied  (mapped, outside the grant)

Then wire the CLI into Codex's hook config (above) and set the three VINCTOR_* env vars in the Codex session. Expected decisions at a glance: a mapped call in the grant → allow; mapped but not in the grant → deny; unmapped → abstain (Codex decides); service down or env missing → deny (fail-closed).

Configuration

Operator policy lives in an optional JSON file at .vinctor/codex-hook.json (override the path with VINCTOR_CODEX_HOOK_CONFIG). It adds or overrides mappings from a Codex tool call to an (action, resource) pair. The file is optional — without it, the hook uses only its built-in mappings.

{
  "version": 1,
  "rules": [
    { "tool": "Bash", "matchType": "prefix", "pattern": "terraform destroy",
      "action": "delete", "resource": "infra/terraform/destroy" },
    { "tool": "apply_patch", "matchType": "glob", "pattern": "**/secrets/**",
      "action": "write", "resource": "secret/custom" },
    { "tool": "mcp__filesystem__read_file", "matchType": "glob", "pattern": "**/etc/**",
      "inputField": "path", "action": "read", "resource": "secret/etc" }
  ]
}

Each rule has: tool (Bash, apply_patch, or mcp__<server>__<tool>), matchType (exact / prefix / glob), pattern, action (one of read/write/execute/deploy/delete/send), and resource. Mandatory fail-closed shell-control checks, pipe-to-shell execution, and destructive force-push classification run first. Among all other mappings, operator rules take precedence and the most specific matching rule wins. A call that matches nothing causes the hook to abstain. Non-destructive Git fetch, pull, and push commands must name an explicit standard URL or filesystem path; default and named remotes can bind executable helpers through repository config and fail closed before operator rules.

Full reference — every field, the per-tool "what pattern matches against" table, apply_patch path semantics, glob/specificity rules, MCP inputField, and worked examples — is in docs/configuration.md.

Inspecting output

The hook writes a single-line JSON decision on stdout (so Codex can parse it), or nothing at all when it abstains. To read a decision by hand, pipe through jq, and use --version / --help to check the binary without sending an event:

node dist/src/cli.js --version
printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm publish"}}' \
  | node dist/src/cli.js | jq

Checking a config or a tool call offline

# Lint a config file (every error at once); exit 0 valid, 1 invalid, 2 unreadable
node dist/src/cli.js validate .vinctor/codex-hook.json --json

# Show how an event would map (action, resource, which rule won); no service call
printf '%s' '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm publish"}}' > /tmp/e.json
node dist/src/cli.js explain /tmp/e.json --json

--json prints a structured object (intended for agents); omit it for human-readable text. explain never calls /v1/enforce.

Self-checking the install with doctor

vinctor-codex-hook doctor          # or: --json

doctor checks everything verifiable from outside a live Codex session and prints a // line per check: the resolved hook-command path (paste into hooks.json), Node ≥ 20, package/plugin-manifest version parity, whether a known hooks.json wires this hook (and warns on an over-broad .* matcher), the VINCTOR_* env (presence only — values are never printed), endpoint reachability (best-effort, no credentials sent), and a classifier smoke (npm publish should map to deploy:pkg/npm/_). Exit 0 when nothing is blocking, 1 on a .

It deliberately cannot prove Codex has trusted or is firing the hook — only /hooks plus an interactive run showing hook: PreToolUse on a mapped command can (headless codex exec silently skips untrusted hooks). doctor prints that caveat every run.

For what each decision and deny code means — and where to look when a permit you expected comes back denied — see docs/troubleshooting.md.

Status

Boundary Preview. Interfaces, configuration shape, and supported tool coverage may change before a stable release. Not production-ready.

Audience

Developers and design partners evaluating runtime authorization boundaries for Codex CLI tool execution.

License

MIT