typed-claude-hooks

Type-safe hooks for Claude Code

Typed inputs and outputs for all 30 hook events.

Read the docs ↓ View on GitHub →
  • Full autocomplete Every property on every event, right in your editor.
  • Smart type narrowing Pass { matcher: "Bash" }, get BashInput. Types follow the matcher.
  • One build command Bundles each handler, adds its mandatory shell wrapper, and updates settings.json.

Manual hooks compared with typed hooks

Manual setup — Claude Code hooks

With typed-claude-hooks

settings.json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": ".claude/hooks/block-rm.sh"
      }]
    }]
  }
}
block-rm.sh
#!/bin/bash
COMMAND=$(jq -r '.tool_input.command')
if echo "$COMMAND" | grep -q 'rm -rf'; then
  exit 2
fi
hooks.config.ts
import { defineHandler } from "@typed-rocks/typed-claude-hooks"

export const blockRm = defineHandler(
  "PreToolUse",
  { matcher: "Bash" },
  async (input) => {
    if (input.tool_input.command.includes("rm -rf")) {
      return {
        hookSpecificOutput: {
          permissionDecision: "deny" as const,
          permissionDecisionReason: "Blocked",
        },
      }
    }
    return {}
  }
)

Get started in 30 seconds

1
$ npx typed-claude-hooks Creates a self-contained .typed-claude-hooks/ project on first run, installs itself into it, then validates and replaces the managed generated directory with each .mjs bundle and mandatory .sh or .ps1 wrapper. Node is the default runtime.
2
Edit .typed-claude-hooks/hooks.config.ts and run the command again. Your project root is never touched, so this works the same in a Python or Go repository.

hookEventName is optional in authored hook output; the runtime injects the handler event when omitted.

Try the Playground with Monaco autocomplete and browser-only downloads, or read the CLI docs.