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

@typed-rocks/typed-claude-hooks

v0.1.5

Published

Type-safe Claude Code hooks in TypeScript

Readme

typed-claude-hooks

Type-safe hooks for Claude Code. All 30 events. Full autocomplete. One build command.

The Problem

Raw Claude Code hooks are shell commands in settings.json. You pipe JSON through stdin, parse it by hand, and hope you spelled the field names right:

#!/usr/bin/env node
const data = require('fs').readFileSync('/dev/stdin', 'utf8');
const input = JSON.parse(data);

// no types — typo in field name? silent bug
if (input.tool_input.comand.includes('rm -rf')) {
  process.exit(2);
}

The Fix

import { defineHandler } from "@typed-rocks/typed-claude-hooks"

export const blockRm = defineHandler("PreToolUse", { matcher: "Bash" }, async (input) => {
  // input.tool_input is fully typed — autocomplete for command, timeout, description
  if (input.tool_input.command.includes("rm -rf")) {
    return {
      hookSpecificOutput: {
        permissionDecision: "deny" as const,
        permissionDecisionReason: "No rm -rf allowed",
      },
    }
  }
  return {}
})
  • Type-safe everything — real TypeScript types for all 30 events, mistakes caught at compile time
  • Smart type narrowing — pass { matcher: "Write" } and get file_path + content; pass { matcher: "Bash" } and get command
  • Test without subprocessestestHandler runs your hook as a function call, no stdin/stdout piping
  • Zero-config settings.json — one command compiles your hooks and generates settings.json

Quick Start

npx typed-claude-hooks

That is the whole setup. On first run it creates a self-contained .typed-claude-hooks/ project, installs itself into it, compiles the example hook, and writes .claude/settings.json:

.typed-claude-hooks/
|-- package.json
|-- hooks.config.ts     <- edit this
|-- tsconfig.json
`-- node_modules/

Nothing is added to your project root, so this works the same in a Python or Go repository as it does in a TypeScript one. Edit hooks.config.ts and run the command again to rebuild.

Prefer to try it first? Open the browser Playground. Monaco provides the package's TypeScript types and autocomplete, while compilation, settings preview, and ZIP creation happen entirely in your browser. Put the downloaded source at .typed-claude-hooks/hooks.config.ts and generated artifacts under .claude/hooks/typed-claude-hooks/. Merge the snippet's hooks property into .claude/settings.json; do not replace your settings file.

The Playground does not execute hooks. It supports one hooks.config.ts only, direct named export const handlers initialized with defineHandler(...), and imports from @typed-rocks/typed-claude-hooks, @typed-rocks/typed-claude-hooks/types, or node:*; arbitrary or extra npm packages and multi-file configs are not supported. Downloads target Node only, not Bun or Deno. Source stays in the page and is not persisted or uploaded.

Writing Hooks

Export handlers as named exports — each is automatically discovered by its event type:

import { defineHandler } from "@typed-rocks/typed-claude-hooks"

// Matcher narrows tool_input to FileWriteInput | FileEditInput
export const protectEnv = defineHandler("PreToolUse", { matcher: "Write|Edit" }, async (input) => {
  if (input.tool_input.file_path.endsWith(".env")) {
    return {
      hookSpecificOutput: {
        permissionDecision: "deny" as const,
        permissionDecisionReason: "Cannot modify .env files",
      },
    }
  }
  return {}
})

// Non-tool events don't use matchers
export const logStop = defineHandler("Stop", async (input) => {
  console.error(`Session stopped: ${input.session_id}`)
  return {}
})

For events with hook-specific output, hookEventName is optional while authoring. The generated runtime inserts the handler's event when it is omitted. You can also provide the exact event explicitly, and TypeScript rejects a mismatched event:

return {
  hookSpecificOutput: {
    hookEventName: "PreToolUse",
    permissionDecision: "deny" as const,
    permissionDecisionReason: "Blocked",
  },
}

defineHandler(event, fn) / defineHandler(event, options, fn)

Creates a typed handler for a specific hook event. For all five tool events — PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, and PermissionDenied — pass a matcher in the options to narrow tool_input to the matched tool's type:

// Matcher narrows tool_input to BashInput — full autocomplete
export const blockRm = defineHandler("PreToolUse", { matcher: "Bash" }, async (input) => {
  input.tool_input.command  // string, no cast needed
})

// Union matcher — tool_input is FileWriteInput | FileEditInput
export const protectEnv = defineHandler("PreToolUse", { matcher: "Write|Edit" }, async (input) => {
  input.tool_input.file_path  // string
})

// No matcher — tool_input stays unknown
export const logAll = defineHandler("PreToolUse", async (input) => { ... })

// Non-tool events don't use matchers
export const onStop = defineHandler("Stop", async (input) => { ... })

Built-in tool inputs are typed for file and search tools, shell and web tools, agents and workflows, tasks and todos, planning and worktrees, notebooks and REPL, cron and wakeups, MCP resources, monitoring, notifications, and remote triggers. This includes tools such as Bash, Read, Write, Edit, Glob, Grep, WebFetch, WebSearch, Agent, AskUserQuestion, NotebookEdit, and the Task* tools. Unknown custom matcher names are accepted with tool_input typed as unknown.

Testing Hooks

Use testHandler to unit test your handlers without stdin/stdout or process spawning:

import { testHandler } from "@typed-rocks/typed-claude-hooks/testing"
import { protectEnv } from "./hooks.config"

const result = await testHandler(protectEnv, {
  tool_name: "Write",
  tool_input: { file_path: ".env", content: "SECRET=123" },
  tool_use_id: "tu_1",
})

expect(result.hookSpecificOutput?.permissionDecision).toBe("deny")

testHandler auto-fills base fields (session_id, cwd, transcript_path) with test defaults. Override any field by including it in the input.

CLI

typed-claude-hooks [config]

Scaffolds the sandbox if needed, then compiles hooks and merges them into the target settings.json.

| Flag | Default | Description | |----------------|-----------------------------------------|-------------------------------------------| | [config] | .typed-claude-hooks/hooks.config.ts | Path to the config file | | -o, --output | .claude/settings.json | Path to the output settings.json | | --hooks-dir | hooks/ next to target | Where to write compiled JS files | | --runtime | node | Wrapper runtime: node, bun, or deno |

Passing an explicit [config] builds that file and skips the sandbox entirely — nothing is scaffolded and no dependency is installed.

Each run checks that the @typed-rocks/typed-claude-hooks version installed in the sandbox matches the CLI's own. On a mismatch it repins that one dependency and reinstalls; any dependencies you added for your own hooks are preserved. A file: or link: specifier is never rewritten.

--runtime applies only to that build. It is embedded in generated wrappers and is not persisted to the config or settings.json; omit it on a later build to return to Node.

Each handler can set shell: "bash" | "powershell" in its options. Bash is the default. Every handler always produces a self-contained .mjs bundle plus a mandatory .sh wrapper for Bash or .ps1 wrapper for PowerShell. The generated settings entry invokes the wrapper, never the .mjs file directly.

export const windowsHook = defineHandler(
  "PreToolUse",
  { matcher: "Bash", shell: "powershell" },
  async () => ({}),
)

typed-claude-hooks init

Scaffolds the sandbox and installs its dependency, then stops. No settings.json is written and no hook artifacts are generated — use it when you want the config and its types in place before wiring anything into Claude Code. Existing files are never overwritten; init reports them as skipped.

How It Works

typed-claude-hooks does three things:

  1. Transpiles your .ts config with esbuild and imports it
  2. Bundles each named handler into a self-contained .mjs file and generates its .sh or .ps1 wrapper
  3. Merges wrapper commands into settings.json, preserving hand-written hooks

For example, blockRm generates:

.claude/hooks/typed-claude-hooks/PreToolUse/
|-- blockRm.mjs
`-- blockRm.sh

The settings command points to blockRm.sh. Generated commands are recognized by their managed directory and replaced on rebuild without touching manual hooks.

Local Development

When working on typed-claude-hooks itself, build and run the CLI from the repo:

npm run build
node dist/cli/index.js

Or use npm link to make the typed-claude-hooks command available globally:

npm link
typed-claude-hooks

Types

All hook types are available as a separate export:

import type {
  HookEvent,
  PreToolUseHookInput,
  StopHookInput,
  SyncHookJSONOutput,
} from "@typed-rocks/typed-claude-hooks/types"

Types are auto-extracted from the @anthropic-ai/claude-agent-sdk package and bundled with typed-claude-hooks — no extra dependencies needed.

License

MIT