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

@fnrhombus/claude-code-hooks

v0.1.1

Published

Strongly-typed TypeScript wrapper for Claude Code hook entrypoints. Write hook scripts as typed functions instead of JSON.parse(process.stdin).

Readme

@fnrhombus/claude-code-hooks

Write Claude Code hooks as typed functions. Not JSON pipelines.

npm ci types license

// your-hook.ts
import { runHook, updateInput } from "@fnrhombus/claude-code-hooks";

runHook({
  preToolUse: {
    Bash: ({ tool_input }) =>
      updateInput({ command: fixPaths(tool_input.command) }),
  },
});

That's the whole hook. No process.stdin.on('data'), no JSON.parse, no branching on hook_event_name, no assembling a hookSpecificOutput envelope by hand. The wrapper handles all of it — including per-tool narrowing, exit codes, and the universal output fields.

Currently the only TypeScript surface for Claude Code's hook API. Types stay in sync with the upstream docs automatically (see How it stays fresh).


Why

Claude Code hooks are a thin protocol: read JSON from stdin, write JSON to stdout, exit with a code. Simple — until you write a second one and realize you're copy-pasting the same JSON.parse / JSON.stringify / event dispatcher every time, and the only way to know what fields exist on a PreToolUse payload is to re-read the docs.

Today you write this:

let raw = "";
process.stdin.on("data", c => raw += c);
process.stdin.on("end", () => {
  const input = JSON.parse(raw);
  if (input.hook_event_name === "PreToolUse" && input.tool_name === "Bash") {
    const fixed = fixPaths(input.tool_input.command);
    if (fixed !== input.tool_input.command) {
      console.log(JSON.stringify({
        hookSpecificOutput: {
          hookEventName: "PreToolUse",
          permissionDecision: "allow",
          updatedInput: { command: fixed },
        },
      }));
    }
  }
});

With @fnrhombus/claude-code-hooks:

import { runHook, updateInput, allow } from "@fnrhombus/claude-code-hooks";

runHook({
  preToolUse: {
    Bash: ({ tool_input }) => {
      const fixed = fixPaths(tool_input.command);
      return fixed === tool_input.command
        ? allow()
        : updateInput({ command: fixed });
    },
  },
});

Fewer lines, fully typed, impossible to misspell hookEventName or forget the envelope.


Features

  • Typed everything. Every hook event, every built-in tool's tool_input, every output field. 26 events × 11 tools × 12 output shapes, all derived from upstream docs.
  • Per-tool narrowing. preToolUse.Bash sees tool_input.command as a string, not unknown. preToolUse.Edit sees file_path, old_string, new_string. No manual casts.
  • Zero-config dispatcher. One call — runHook({ ... }) — handles stdin parsing, routing by event, envelope assembly, stdout serialization, and exit codes.
  • Shorthand helpers. allow(), deny(reason), updateInput({...}), block(reason), halt(reason), addContext(text) — no more hand-rolling the output JSON.
  • Escape hatches. throw new HookBlock(reason) exits with code 2 (blocking). Every handler can also return universal fields like continue: false. Return undefined or void for no-op.
  • Dual package. ESM + CJS both work. Bring-your-own runtime: Node 20+.
  • Automatically fresh. The type definitions are regenerated from the upstream docs and published on every upstream change — see How it stays fresh.

Install

npm  i  @fnrhombus/claude-code-hooks
pnpm add @fnrhombus/claude-code-hooks
yarn add @fnrhombus/claude-code-hooks

Requires Node 20+. No runtime dependencies.


60-second tour

Rewrite a command before it runs (the @fnrhombus/claude-code-pathfix use case):

import { runHook, updateInput } from "@fnrhombus/claude-code-hooks";

runHook({
  preToolUse: {
    Bash: ({ tool_input }) => {
      const fixed = tool_input.command.replace(/^cd C:\\/, "cd /c/");
      return { updatedInput: { command: fixed } };
    },
  },
});

Deny dangerous commands:

import { runHook, deny, allow } from "@fnrhombus/claude-code-hooks";

runHook({
  preToolUse: {
    Bash: ({ tool_input }) =>
      /rm\s+-rf\s+\//.test(tool_input.command)
        ? deny("refusing to rm -rf /")
        : allow(),
  },
});

Inject context on session start:

import { runHook, addContext } from "@fnrhombus/claude-code-hooks";

runHook({
  sessionStart: () => addContext(`Build status: ${getCIStatus()}`),
});

Handle multiple events in one hook:

import { runHook, allow, addContext } from "@fnrhombus/claude-code-hooks";

runHook({
  sessionStart: () => addContext("welcome back"),
  preToolUse: {
    Bash: ({ tool_input }) => lintCommand(tool_input.command),
    Edit: ({ tool_input }) => guardProtectedFiles(tool_input.file_path),
    default: () => allow(),
  },
  userPromptSubmit: ({ prompt }) =>
    prompt.includes("API_KEY") ? { block: "prompt contains a secret" } : undefined,
});

Block stopping when work is unfinished:

import { runHook, block } from "@fnrhombus/claude-code-hooks";

runHook({
  stop: () => hasUnfinishedWork() ? block("tests still failing") : undefined,
});

How it stays fresh

The upstream hooks API is documented as markdown. This repo bakes the SHA256 of that markdown into the first line of packages/core/src/types.ts. A scheduled job runs scripts/dev-cycle once a day:

  1. Fetch hooks.md, compare its hash to the one in types.ts
  2. If unchanged, exit immediately (fully deterministic, no LLM reasoning)
  3. If changed:
    • File a tracking GitHub issue
    • Create a feature branch + worktree
    • Regenerate types.ts via the regen-hook-types Claude skill
    • Run the full test suite and fix wrapper code until it passes (with retry + research phases)
    • Push, open a PR, wait for CI, run an automated PR review, merge
    • Clean up the branch and worktree
  4. If the loop gives up, assign the issue to @fnrhombus and log an entry to BLOCKERS.md

The effect: users never need to know when Anthropic updates the hook API. Semver-compatible changes go straight to npm; breaking changes become a version bump in a PR that merges itself on green.


What's in the box

| Export | Purpose | |---|---| | runHook(handlers) | Main entrypoint — reads stdin, dispatches, writes stdout, exits | | dispatch(handlers, input) | Pure function version — no I/O, good for tests | | allow(), deny(reason), ask(reason), defer(reason?) | PreToolUse decisions | | updateInput(input) | Rewrite the tool input in-flight (no retry cost) | | block(reason) | Block with a reason (UserPromptSubmit, PostToolUse, Stop, etc.) | | halt(reason) | Stop Claude entirely (universal) | | addContext(text) | Inject text into Claude's context (SessionStart, UserPromptSubmit, …) | | setSessionTitle(title) | Set the session title | | HookBlock | throw new HookBlock(reason) → exit 2 with reason | | type HookInput, type HookOutput, type HookHandlers, … | Every type you could want — see types.ts |


Related

  • fnrhombus/claude-code-pathfix — the hook that inspired this package. Transparently rewrites Windows paths to POSIX in Bash commands so Claude Code on Windows stops wasting tokens on retry loops. Uses this library as its typed backbone.

License

MIT © fnrhombus. See LICENSE.

Support