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

exit2fix

v2.0.0

Published

Static + live-canary doctor for Claude Code enforcement hooks that fail open on deny paths

Readme

exit2fix

CI npm license node status

Your Claude Code security hook probably isn't blocking anything. Here's how to prove it and fix it in one command.

npx exit2fix
npx exit2fix --fix --yes

Claude Code's hook exit-code contract is counter-intuitive. Developers ship PreToolUse "security gates" that exit 1 (or crash) on the deny path — and Claude Code lets the action through. The only exit code that blocks on its own is 2.

Claude Code is a product of Anthropic. This project is independent and unaffiliated. Hook behavior can change between Claude Code versions — see version boundaries below and docs/architecture.md.

Docs: CHANGELOG · Contributing · Security · Architecture · Demo recording notes

The exit-code contract

| Exit code | Meaning for most events (PreToolUse, etc.) | | --- | --- | | 0 | Success. Proceed. Stdout may be parsed as JSON. | | 2 | Block. Stderr (or JSON decision reason) is the message. Overrides even a JSON "allow". | | Anything else (including 1, uncaught exceptions, missing shebang / not executable / exit 127) | Non-blocking error. Action proceeds. A hook-error notice appears in the transcript (Ctrl+O), but nothing is stopped. |

Nuances exit2fix encodes:

  • Valid deny JSON on stdout (permissionDecision: "deny") still blocks even if you also exit 1.
  • WorktreeCreate is inverted: any nonzero exit fails creation (so exit 1 is fine there).
  • PermissionRequest ignores exit 2 — deny via the decision JSON object.
  • Events like PostToolUse, Setup, SessionStart, Notification can never block; deny logic on them is a different bug.
  • HTTP hooks cannot block via status code — need 2xx + deny JSON.
  • Timed-out PreToolUse command hooks do not block.
  • Version boundaries: as of Claude Code v2.1.214, exit 2 + invalid stdout JSON still blocks. v2.1.248 changed unparseable-stdout handling. Live mode notes when your install is older.

Source: Claude Code hooks docs.

Why Anthropic's hook-linter.sh misses this

The official Check 9 in anthropics/claude-code plugin-dev skill is:

if ! grep -q "exit 0\|exit 2" "$script"; then
  echo "⚠️ No explicit exit codes (should exit 0 or 2)"
fi

That is a whole-file substring search. It passes as soon as the string exit 2 appears anywhere — in a comment, an unreachable branch, or next to an exit 1 on the actual deny path. It will not catch the exact bug exit2fix targets, and it is bash-only.

What exit2fix does

  1. Discovery — .claude/settings.json, settings.local.json, ~/.claude/settings.json, plugin hooks/hooks.json, skill/agent frontmatter, and managed policy settings.
  2. Static analysis — tree-sitter for bash, Python, Node/TS, PowerShell (deny-path exit codes, uncaught crashes, shebang mismatch, mixed allow-JSON, event misuse).
  3. Registration checks — missing/high timeout on blocking events, HTTP hooks that can't block via status, PermissionRequest exit-2 misuse.
  4. Tier-1 harness probe (default) — pipes documented stdin JSON; confirms fail-open without an API key.
  5. Tier-2 --live — scratch project + claude -p --output-format stream-json --verbose; parses hook_response exit codes.
  6. --fix — precise deny-path rewrites + shebang / bash ERR trap; re-probes and prints block confirmed.

Pairs well with claude-hooks-check for hooks.json schema / matcher linting — exit2fix does not duplicate that.

Install

npm install -g exit2fix
# or
npx exit2fix

Usage

# Scan current project (static + harness probe)
exit2fix

# JSON / SARIF for CI
exit2fix --json --fail-on CRITICAL
exit2fix --sarif > exit2fix.sarif

# Autofix + re-probe
exit2fix --fix --yes

# Optional live Claude session canary
exit2fix --live

# Static only
exit2fix --no-probe

GitHub Action

- uses: Onur45500/exit2fix@v2
  with:
    path: .
    fail-on: CRITICAL
    sarif: "true"

See action.yml. Pre-commit examples live in examples/.

CI example

- name: exit2fix
  run: npx exit2fix --json --fail-on CRITICAL

Config (optional) .exit2fix.json

{
  "denyKeywords": ["deny", "blocked", "forbidden", "not allowed"],
  "failOn": ["CRITICAL"],
  "triggers": [
    {
      "id": "custom-rm",
      "events": ["PreToolUse"],
      "matchers": ["Bash"],
      "stdin": {
        "hook_event_name": "PreToolUse",
        "tool_name": "Bash",
        "tool_input": { "command": "rm -rf /" }
      }
    }
  ]
}

Demo

Before: canary deny input → exit 1 → action would proceed.
After: same input → exit 2 → block confirmed.

Want a real terminal GIF later? See docs/demo.md.

Finding severities

| Class | Meaning | | --- | --- | | CRITICAL confirmed | Static finding + harness (or live) probe proved fail-open | | CRITICAL static-only | Suspicious deny path / HTTP policy hook; probe skipped or inconclusive | | WARN | Timeout, shebang, PermissionRequest, non-blocking event, mixed allow-JSON, set -e risk | | INFO | Informational / fixed-and-confirmed / version notes |

Development

npm install --legacy-peer-deps
npm test
npm run build

# Optional live integration (needs claude CLI + auth)
EXIT2FIX_LIVE=1 npm test

See CONTRIBUTING.md.

License

MIT — see LICENSE.