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-code-safety-hooks

v0.1.0

Published

Generic, config-driven safety hooks for Claude Code — free stuck dev ports, block force-push, enforce Conventional Commits, require tests, and gate deploys, all wired via Claude Code's PreToolUse/Stop hook model.

Readme

claude-code-safety-hooks

Guardrails for Claude Code that stop the most common self-inflicted footguns — before they happen.

npm version license: MIT node >=18

A small, dependency-free set of config-driven hook scripts you drop into any repo. They plug into Claude Code's PreToolUse and Stop events and quietly enforce the boring-but-important stuff: don't force-push over a teammate, write a sane commit message, don't ship to prod by accident, don't leave code untested, and don't let a stuck dev server block the next run.

npx claude-code-safety-hooks init

[demo GIF placeholder — record with asciinema / vhs: npx claude-code-safety-hooks init → paste the snippet → watch a force-push get blocked. Drop the .gif here.]


What is this?

When you let Claude Code run Bash autonomously, it's fast — and occasionally too fast. It can git push --force onto a shared branch, fire off a deploy command, commit with a message that breaks your changelog tooling, or spin up a dev server on a port that's already taken. None of these are Claude being "wrong" exactly; they're the kind of slip a tired human makes too. The fix is the same one teams already use: cheap, deterministic guardrails.

Claude Code lets you register shell scripts that run before a tool call (and when the agent stops). A hook that exits non-zero can deny the action and hand its stderr back to the model so it corrects course. This package is a curated set of those hooks, extracted from a production monorepo and fully genericized — every project-specific value lives in a config file or an env var, nothing is hardcoded.

  • Zero runtime dependencies. Pure bash/sh + python3 (for JSON parsing — present on macOS and virtually every dev box), with graceful fallbacks.
  • Config-driven. Ports, commit types, deploy patterns, test globs — all in one JSON file.
  • Fail-open. A malformed payload never wedges your session; hooks only block when they're sure.
  • Non-destructive install. init prints the settings snippet; it never edits your settings.json.

The hooks

| Hook | Event | What it does | |------|-------|--------------| | guard-dev-ports.sh | PreToolUse:Bash | Frees your configured dev-server ports before a dev command runs, so EADDRINUSE never blocks startup. | | block-force-push.sh | PreToolUse:Bash | Denies git push --force / --force-with-lease. Branch deletes are still allowed. | | lint-commit-msg.sh | PreToolUse:Bash | Enforces Conventional Commits (type(scope): summary) on every git commit. | | require-tests.sh | Stop | Warns (or blocks) when staged source files have no matching test file. | | confirm-destructive-deploy.sh | PreToolUse:Bash | Blocks a configurable "deploy/release" command unless an explicit confirmation env var is set. |


Quick start

From the root of the repo where you use Claude Code:

npx claude-code-safety-hooks init

This will:

  1. Copy the hook scripts into ./.claude/hooks/ (and chmod +x them).
  2. Write a default claude-code-safety-hooks.config.json at your repo root (if you don't already have one).
  3. Print the exact .claude/settings.json snippet to wire the hooks up. It does not modify your settings.json for you — you merge it, so nothing you've already configured gets clobbered.

Then paste the printed hooks block into .claude/settings.json (see next section), tweak the config to taste, and restart Claude Code. Done.

npx claude-code-safety-hooks --help lists everything.


Wiring it up (.claude/settings.json)

init prints this exact snippet. If you already have a .claude/settings.json, merge the hooks block into it — append to any existing PreToolUse / Stop arrays rather than replacing them.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": ".claude/hooks/guard-dev-ports.sh" }]
      },
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": ".claude/hooks/block-force-push.sh" }]
      },
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": ".claude/hooks/lint-commit-msg.sh" }]
      },
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": ".claude/hooks/confirm-destructive-deploy.sh" }]
      }
    ],
    "Stop": [
      {
        "hooks": [{ "type": "command", "command": ".claude/hooks/require-tests.sh" }]
      }
    ]
  }
}

Global vs. project. The snippet above wires the hooks per-project (.claude/settings.json). To apply them to every repo, put the same hooks block in your global ~/.claude/settings.json and copy .claude/hooks/ (plus a config) into each repo — the hooks resolve config relative to the repo they run in.


Configuration

All behavior is driven by claude-code-safety-hooks.config.json at your repo root (the default written by init is shown below). Every value can also be overridden by an environment variable — handy for CI or one-off escape hatches. The env name is the config key, uppercased with dots → underscores, prefixed CCSH_ (e.g. commit.maxLengthCCSH_COMMIT_MAXLENGTH).

{
  "devPorts": {
    // Ports freed before a dev command starts.
    "ports": [3000, 3001, 5173, 8080, 8081],
    // Substrings that identify a "dev server" command.
    "triggers": ["npm run dev", "pnpm dev", "pnpm run dev", "yarn dev",
                 "next dev", "vite", "expo start"]
  },
  "forcePush": {
    "enabled": true                 // master switch for block-force-push.sh
  },
  "commit": {
    "types": ["feat", "fix", "refactor", "perf", "style", "docs",
              "test", "chore", "revert", "build", "ci"],
    "scopes": [],                   // [] = accept any scope token; list them to restrict
    "requireScope": false,          // true = a scope is mandatory: type(scope): ...
    "maxLength": 72,                // subject-line length cap
    "lowercaseSubject": true,       // summary must start lowercase
    "noTrailingPeriod": true        // summary must not end with "."
  },
  "tests": {
    "mode": "warn",                 // "warn" (advisory) | "block" (deny on Stop)
    "sourceGlobs": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx",
                    "lib/**/*.ts", "lib/**/*.js"],
    "testPatterns": ["{dir}/{name}.test.{ext}",
                     "{dir}/{name}.spec.{ext}",
                     "{dir}/__tests__/{name}.test.{ext}"],
    "exclude": ["**/*.d.ts", "**/index.ts", "**/index.js", "**/*.config.*",
                "**/*.test.*", "**/*.spec.*", "**/types.ts", "**/constants.ts"]
  },
  "deploy": {
    "commandPattern": "(deploy|release)\\b",   // extended-regex marking a deploy
    "confirmEnv": "CCSH_ALLOW_DEPLOY",         // env var that unlocks it
    "message": "…"                             // optional custom block message
  }
}

Config discovery order (first hit wins): $CCSH_CONFIG./claude-code-safety-hooks.config.json./.claude/claude-code-safety-hooks.config.json → the same two at the git root → the copy shipped next to the hooks. So the defaults work even before you write your own.


Per-hook detail

1. guard-dev-ports.sh

Problem: you ask Claude to restart the dev server, the old one is still bound, and you get Error: listen EADDRINUSE :::3000.

What it does: when a Bash command matches one of devPorts.triggers, it frees every port in devPorts.ports (via lsof, falling back to fuser) so the server starts clean. It is advisory — always exits 0, never blocks the command; it only ever touches ports you've told it to manage.

Customize:

"devPorts": { "ports": [3000, 4000], "triggers": ["npm start", "make serve"] }
CCSH_DEVPORTS_PORTS="3000,4000"          # comma/space separated
CCSH_DISABLE_GUARD_DEV_PORTS=1           # turn it off

2. block-force-push.sh

Problem: git push --force onto a shared branch silently discards commits other people already pushed.

What it does: denies any git push carrying -f, --force, --force-with-lease, or --force-if-includes (exit 2, with an explanation returned to the model). Deleting a remote branch (git push origin --delete X or git push origin :X) is not a history rewrite and is allowed through.

Customize:

CCSH_FORCEPUSH_ENABLED=false             # disable entirely
CCSH_ALLOW_FORCE_PUSH=1                   # one-off escape hatch for a single push

3. lint-commit-msg.sh

Problem: inconsistent commit subjects break changelog generation and make history hard to scan.

What it does: parses the commit message out of the git commit command (both -m "…" and <<'EOF' … EOF heredoc forms) and validates it against type(scope): summary — configurable type list, optional-or-required scope, lowercase summary, no trailing period, length cap. A breaking-change ! (feat!: …) is accepted. Commits with no visible message (plain git commit opening an editor, -F file, --amend --no-edit) are left alone.

Customize:

"commit": { "types": ["feat","fix","docs"], "scopes": ["api","web"],
            "requireScope": true, "maxLength": 50 }
CCSH_COMMIT_MAXLENGTH=50
CCSH_DISABLE_LINT_COMMIT_MSG=1

4. require-tests.sh

Problem: "every change ships with a test" is a great norm that's easy to forget mid-session.

What it does: on Stop, inspects your staged source files (things you deliberately git added). For each file matching tests.sourceGlobs (and not in tests.exclude), it checks whether a matching test exists per tests.testPatterns. In warn mode it prints the gaps and exits 0; in block mode it exits 2 and asks the model to add tests. Deletions are exempt (removing a module with its test is valid).

Customize:

"tests": { "mode": "block",
           "sourceGlobs": ["packages/**/src/**/*.ts"],
           "testPatterns": ["{dir}/{name}.test.{ext}", "{dir}/__tests__/{name}.test.{ext}"],
           "exclude": ["**/*.d.ts", "**/index.ts", "**/infra/**"] }
CCSH_TESTS_MODE=block
CCSH_DISABLE_REQUIRE_TESTS=1

Tip: the exclude list is where hard-to-unit-test infra goes — SDK wrappers, generated clients, barrel index files, config. Add globs there rather than sprinkling skip comments through your code.

5. confirm-destructive-deploy.sh

Problem: a deploy/release command run by accident ships straight to production.

What it does: blocks any command matching deploy.commandPattern unless the deploy.confirmEnv variable is set (either exported into the environment, or typed inline as CCSH_ALLOW_DEPLOY=1 <cmd>). This encodes the staging-before-prod discipline generically: shipping becomes a deliberate, auditable step.

Customize:

"deploy": { "commandPattern": "kubectl apply|helm upgrade|serverless deploy",
            "confirmEnv": "I_HAVE_TESTED_ON_STAGING" }
CCSH_ALLOW_DEPLOY=1 npm run deploy       # the sanctioned way to actually deploy
CCSH_DEPLOY_COMMANDPATTERN='gcloud .*deploy'
CCSH_DISABLE_CONFIRM_DEPLOY=1

Why hooks?

Claude Code fires hooks at defined points in the agent loop. Two matter here:

  • PreToolUse runs before a tool executes. The hook receives the tool call as JSON on stdin — e.g. {"tool_name":"Bash","tool_input":{"command":"git push --force"}}. Its exit code is the verdict:
    • 0 → allow (stdout is shown to you, ignored by the model)
    • 2deny; the hook's stderr is fed back to the model, so it sees why and can adjust
    • any other code → non-blocking error (shown to you)
  • Stop runs when the agent finishes its turn — the natural place to check "did we leave the tree in a good state?" (e.g. staged code without tests). A Stop hook returning 2 nudges the model to keep working.

Because the model reads a blocked hook's stderr, these guardrails are teaching as much as blocking: a denied force-push tells Claude to use a normal push; a rejected commit message shows the exact format it should use. Deterministic, fast (milliseconds), and impossible to "forget" — which is exactly what you want protecting the irreversible actions.

Every hook here fails open: if it can't parse the payload, it exits 0 and gets out of the way. It only ever blocks when it's confident.


Compatibility

  • Node ≥ 18 for the installer (bin/cli.js). The hooks themselves are bash/sh.
  • python3 is used for robust JSON parsing (default on macOS; standard on Linux dev images). If it's absent, the hooks fall back to a sed-based extractor for the common cases and otherwise fail open.
  • lsof (or fuser) is used by guard-dev-ports.sh; if neither is present it no-ops.
  • Tested on macOS (bash 3.2 / zsh) and Linux.

Development

git clone https://github.com/oratis/claude-code-safety-hooks
cd claude-code-safety-hooks
npm test          # runs test/run.sh — execs every hook with sample payloads

The test harness asserts exit codes for each hook (force-push denied, good commit passes, bad one fails, deploy gated, tests required, …) and exits non-zero on any mismatch.

Contributing

Issues and PRs welcome. Good first additions: more testPatterns presets, extra dev-server triggers, or new opt-in hooks that follow the same fail-open, config-driven shape.

License

MIT © 2026 oratis