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

claude-hook-guard

v0.1.0

Published

Production-grade Claude Code hooks. Fail closed by default, audit every run, never silently break.

Readme

claude-hook-guard

Production-grade base classes for Claude Code hooks. Fail closed by default, audit every run, never silently break.

A Claude Code hook is a tiny program: it reads a JSON event on stdin, decides what to do, and writes a JSON decision on stdout. Most hooks people share are a bare stdin reader wrapped in a try/catch that swallows errors. That has a nasty failure mode — when the hook itself breaks, it fails open: the action it was supposed to guard sails right through, and nothing tells you the guard stopped working.

claude-hook-guard gives you a small set of base classes that own the lifecycle so that can't happen:

  • Fail closed by default — if your check throws, or the hook itself errors, a protection hook blocks. Safety is never an accident of error handling.
  • Audited every run — every invocation appends a structured NDJSON record (success, error, or bypass). You can always answer "did the guard run, and what did it decide?"
  • Bounded — a hard 5-second timeout means a hung hook can't wedge your session.
  • One decision, guaranteed — the base owns stdout, so you can't accidentally emit two conflicting decisions.
  • An operator escape hatch — a kill-switch and a time-boxed bypass token, because a guard you can't turn off in an emergency is its own hazard.

Zero dependencies. Node 18+.


Quick start

Get two battle-tested guards running in your project in one command:

npm install claude-hook-guard
npx claude-hook-guard init        # adds the bundled hooks to ./.claude/settings.json

init is idempotent and backs up any existing settings file. Use --user to target ~/.claude/settings.json instead. Start a new Claude Code session and the guards are live.

Or install just the library to build your own hooks:

npm install claude-hook-guard

Bundled hooks

Two ready-to-use PreToolUse hooks, both built on the base classes below:

  • dangerous-command-guard — denies destructive commands (rm -rf /, rm -rf ~, git push --force, git reset --hard, mkfs, dd to a disk, fork bombs, …) and writes to secret files (.env, *.pem, SSH keys, AWS credentials). Quote-aware, so echo "rm -rf /" is fine.
  • cloud-sync-git-guard — blocks edits in a git repo that a file-sync service (Dropbox, iCloud, OneDrive, Google Drive, Synology) has likely corrupted: conflicted-copy files, a tree far behind the remote, or a tree missing many files. Catches the case where syncing .git across machines leaves you about to push deletions you never made.

Need to run a blocked action once? Prefix it with CHK_BYPASS=1 — the bypass is recorded in the audit log.

Pick the base that matches your hook

| Base | Use it when… | On error | |---|---|---| | ProtectionBase | You block a disallowed action (guard a path, refuse a destructive command) | Fail closed — block | | ValidationBase | You run several checks and want to report all failures at once | Fail closed — block | | LoggingBase | You observe/record and must not get in the user's way | Fail open on your logic; fail closed on infra errors | | AuditBase | You record an event that must never be missed | Fail closed — a missing record is the failure | | HookBase | You need full control | Explicit fail closed (override onError) |

Example: block edits to protected files

#!/usr/bin/env node
const { ProtectionBase, PolicyViolation } = require('claude-hook-guard');

class ProtectPaths extends ProtectionBase {
  async execute(input) {
    const file = input.tool_input?.file_path || '';
    if (/(^|\/)\.env(\.|$)|\.pem$|id_rsa$/.test(file)) {
      throw new PolicyViolation(`Refusing to edit a secret file: ${file}`);
    }
  }
}

new ProtectPaths({ hookName: 'protect-paths' }).run();

Wire it up in .claude/settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "node ~/.claude/hooks/protect-paths.js" }]
      }
    ]
  }
}

If execute() returns normally, the action is allowed. If it throws PolicyViolation — or if the hook itself crashes — the action is blocked and the reason is shown to Claude. Either way an audit line is written to ~/.claude/hooks/.audit/protect-paths.ndjson.

Example: a Stop hook that checks the job is actually done

#!/usr/bin/env node
const { ValidationBase } = require('claude-hook-guard');

class DoneCheck extends ValidationBase {
  async execute(input) {
    this.addCheck('tests-ran', () => input.ranTests ? null : 'no test run recorded');
    this.addCheck('no-todos', (ctx) => ctx.hasTodos ? 'unresolved TODOs remain' : null);
    await this.runChecks({ hasTodos: false });
  }
}

new DoneCheck({ hookName: 'done-check' }).run();

runChecks runs every check and reports them together, so you see all failures at once instead of one per round-trip.

How a block maps to Claude Code's contract

Getting this wrong is the most common way a hook silently fails open, so the kit handles it for you. When a protection/validation hook blocks, it emits the shape the current Claude Code event expects and exits with the code that actually enforces it:

| Hook event | Emitted on block | Exit | |---|---|---| | PreToolUse | hookSpecificOutput.permissionDecision: "deny" (the legacy decision: "approve"/"block" is deprecated for this event) | 0 | | Stop / SubagentStop | top-level decision: "block" + reason | 0 | | anything else, or if stdin never parsed | reason on stderr | 2 |

With valid JSON the correct exit code is 0 — the decision rides in the JSON, not the status code. Exit 2 is Claude Code's blocking-error signal (stderr shown). Exit 1 is a non-blocking error: the JSON is ignored and the action proceeds, so the kit never uses it to block. The hook reads hook_event_name from stdin to pick the right format automatically.

Audit records

Every run appends one line to ~/.claude/hooks/.audit/<hookName>.ndjson:

{"ts":"2026-05-24T03:28:00.000Z","hook":"protect-paths","runId":"...","step":0,"jobId":"","workerTokenHash":"","recoveryPath":"fail-closed-block","errorClass":"PolicyViolation","outcome":"error"}

Never log raw secrets — hash any worker/identity value into workerTokenHash.

The directory follows CLAUDE_CONFIG_DIR if you've set it, otherwise ~/.claude.

Operator escape hatches

A guard you can't disable in an emergency is a hazard. Three layers, checked before any hook logic runs:

  • CHK_BYPASS=1 env var — bypass for a single command.
  • A time-boxed bypass token — bypass for N seconds, then it auto-expires: npx claude-hook-guard bypass issue 120 "reason" (and revoke / status).
  • A kill-switch lock file — bypass every hook until you turn it off: npx claude-hook-guard kill-switch on (and off / status).

Every bypass is itself written to the audit log, so an escape hatch is never silent.

Design

Each base is a template method: you implement execute(input), and the base handles stdin parsing, the timeout, the single stdout decision, audit logging, and the error policy. Subclasses should not write their own try/catch, call process.exit(), or write to stdout directly — those are exactly the things that let a hook fail open, and the base owns them.

Roadmap

  • [x] Base classes (HookBase, ProtectionBase, ValidationBase, LoggingBase, AuditBase)
  • [x] npx claude-hook-guard init — patch settings.json
  • [x] Bundled hooks: dangerous-command-guard, cloud-sync-git-guard
  • [x] Kill-switch / bypass-token CLI
  • [ ] init scaffolder for a new custom hook from a template
  • [ ] More bundled hooks (secret-scanner on Write, audit-trail logger)

License

MIT