Documentation
Typed hooks, generated safely
Author Claude Code hooks as named TypeScript exports, test them as functions, and build runtime-ready wrapper commands.
Install and build
Run the command. On first run it creates a self-contained .typed-claude-hooks/ project — package.json, hooks.config.ts, a strict tsconfig.json, and a .gitignore — installs itself into it, and builds. Your project root is never touched, so this works the same in a Python or Go repository.
npx typed-claude-hooksExisting files are never overwritten. Each run repins and reinstalls the sandbox dependency when its version drifts from the CLI's, leaving any dependencies you added for your own hooks alone. A file: or link: specifier is never rewritten.
To scaffold and install without building — no settings.json, no generated hooks — run init instead. It is the same setup step the command above performs on its own, stopped before the build.
npx typed-claude-hooks initThe generated config uses the current authoring API:
import { defineHandler } from "@typed-rocks/typed-claude-hooks"
export const protectEnvFiles = 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 {}
})Browser Playground
Open the Playground to author a hook with Monaco types and autocomplete. It compiles in your browser, previews the generated settings and file destinations, and downloads the source, Node artifacts, wrappers, and placement instructions as a ZIP.
Place the editable source at .typed-claude-hooks/hooks.config.ts and generated artifacts under .claude/hooks/typed-claude-hooks/. The downloaded settings file is a snippet: merge its hooks property into .claude/settings.json; do not replace your existing settings.
The Playground does not execute hooks. It accepts one hooks.config.ts with direct named export const handlers initialized by defineHandler(...). Multi-file configs and arbitrary or extra npm packages are not supported; imports are limited to @typed-rocks/typed-claude-hooks, @typed-rocks/typed-claude-hooks/types, and node:*. Downloads target Node only, not Bun or Deno. Source is not persisted or uploaded.
Author handlers
defineHandler is the config authoring API. Export every handler by name; the compiler discovers named exports and groups them by their event field. There is no default config export.
Use defineHandler(event, handler) or defineHandler(event, options, handler). The event selects the exact input and output types.
Events and tool inputs
All 30 Claude Code hook events are typed. Five events carry tool data and support matcher-aware narrowing: PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, and PermissionDenied.
Tool lifecycle
PreToolUsePostToolUsePostToolUseFailurePostToolBatchPermissionRequestPermissionDenied
Session lifecycle
SessionStartSessionEndStopStopFailureSetupPreCompactPostCompact
Agents and tasks
SubagentStartSubagentStopTeammateIdleTaskCreatedTaskCompleted
Interaction
NotificationUserPromptSubmitUserPromptExpansionElicitationElicitationResultMessageDisplay
Workspace
ConfigChangeWorktreeCreateWorktreeRemoveInstructionsLoadedCwdChangedFileChanged
A matcher such as { matcher: "Bash" } narrows tool_input to BashInput. Union matchers such as Write|Edit produce a matching input union. The included SDK map covers 34 built-in tool inputs; custom matcher names remain valid with tool_input typed as unknown.
Typed output
Hook output is constrained to the selected event. For events with hookSpecificOutput, hookEventName is optional while authoring and is strongly typed if supplied. The generated runtime injects the input event name when the property is omitted.
Test handlers
testHandler invokes a handler directly without stdin, stdout, or a subprocess. It fills session_id, cwd, and transcript_path with test defaults; provide the event-specific fields.
import { testHandler } from "@typed-rocks/typed-claude-hooks/testing"
import { protectEnvFiles } from "./hooks.config"
const result = await testHandler(protectEnvFiles, {
tool_name: "Write",
tool_input: { file_path: ".env", content: "SECRET=123" },
tool_use_id: "tool_1",
})
expect(result.hookSpecificOutput?.permissionDecision).toBe("deny")CLI
Build with npx typed-claude-hooks [config]. Passing an explicit config path builds that file and skips the sandbox entirely. The command has four inputs:
| Input | Default | Purpose |
|---|---|---|
[config] | .typed-claude-hooks/hooks.config.ts | Config path |
-o, --output | .claude/settings.json | Target settings file |
--hooks-dir | hooks/ beside settings | Generated hooks root |
--runtime | node | node, bun, or deno |
Runtime selection is CLI-only and applies to that build. It is not persisted in config or settings, so a later build without --runtime returns to Node.
Generated files
Every handler produces a self-contained .mjs bundle and a mandatory shell wrapper. Bash is the default and generates .sh; select PowerShell per handler to generate .ps1:
export const windowsHook = defineHandler(
"PreToolUse",
{ matcher: "Bash", shell: "powershell" },
async () => ({}),
).claude/
|-- settings.json
`-- hooks/
`-- typed-claude-hooks/
`-- PreToolUse/
|-- protectEnvFiles.mjs
`-- protectEnvFiles.sh Generated settings commands always invoke the .sh or .ps1 wrapper, never the .mjs bundle directly. The wrapper checks the selected runtime and launches the adjacent bundle.
Build semantics
The build first loads and validates the config, extracts handlers, bundles all artifacts in memory, parses existing settings, and computes the merged settings. Validation errors therefore leave existing generated output untouched.
After validation succeeds, the build deletes and replaces the exact typed-claude-hooks managed directory under the selected hooks root. It writes generated files and settings directly, while preserving non-managed settings entries. Because these filesystem writes are not transactional, a disk or permission failure during this phase can leave partial generated output or settings that were not updated.