@dawkinsuke/hooks
v0.1.4
Published
Claude Code hooks toolkit — write hooks with TypeScript, compile to settings.local.json
Downloads
959
Readme
@dawkinsuke/hooks
Claude Code hooks toolkit — write hooks in TypeScript, compile to settings.local.json.
Write TypeScript extensions → hx build → hooks & settings readyPrerequisites
- Bun v1.0.0 or later
Install
bun install -g @dawkinsuke/hooksOptionally, install the Claude Code plugin so Claude can create hooks for you in natural language:
# In Claude Code
/plugin marketplace add ashigirl96/hx-cli
/plugin install hook-creator@dawkinsuke-hx-cliQuick Start
With the plugin installed, ask Claude to create hooks in natural language:
> /hook-creator Create a hook that blocks git pushThis generates .claude/extensions/block-push/index.ts:
import { defineExtension, deny } from "@dawkinsuke/hooks"
export default defineExtension((cc) => {
cc.on("PreToolUse", "Bash", async (input) => {
if (input.tool_input.command && /git\s+push/.test(input.tool_input.command)) {
return deny("git push is blocked by hook policy")
}
})
})You can keep going — just describe what you want:
> Create a hook that runs bun test before git commit when src/ files are stagedimport { execSync } from "node:child_process"
import { defineExtension, modifyInput } from "@dawkinsuke/hooks"
export default defineExtension((cc) => {
cc.on("PreToolUse", "Bash", async (input) => {
const command = input.tool_input.command as string | undefined
if (!command || !/git\s+commit/.test(command)) return
const staged = execSync("git diff --cached --name-only", { encoding: "utf-8" })
const hasSrcChanges = staged
.split("\n")
.filter(Boolean)
.some((f) => f.startsWith("src/"))
if (hasSrcChanges) {
return modifyInput({ ...input.tool_input, command: `bun test && ${command}` })
}
})
})The hook-creator skill automatically runs hx build — hooks are active immediately 🎉
Use hx activate to toggle extensions on/off.
Writing Extensions
Create .claude/extensions/<name>/index.ts (or use hx new <name> to scaffold):
import { defineExtension, deny, addContext } from "@dawkinsuke/hooks"
export default defineExtension((cc) => {
// Command hook — compiled to .mjs, full Bun runtime access
cc.on("PreToolUse", "Bash", async (input) => {
if (input.tool_input.command?.match(/rm\s+-rf\s+\//)) {
return deny("Destructive command blocked")
}
})
// HTTP hook — POST to a URL on events (declarative, not compiled)
cc.http("PostToolUse", {
matcher: "Bash",
url: "http://localhost:8080/audit",
timeout: 5,
})
// Prompt hook — single-turn LLM evaluation (declarative)
cc.prompt("PreToolUse", {
matcher: "Edit",
prompt: "Ensure the edit does not introduce security vulnerabilities.",
})
// Agent hook — multi-turn LLM verification (declarative)
cc.agent("PostToolUse", {
matcher: "Write",
prompt: "Verify the file is syntactically correct.",
})
})| Type | Method | Compiled | Use Case |
| ------- | ------------- | -------- | ---------------------------------------------------------------------- |
| Command | cc.on() | .mjs | Programmatic logic with full Bun access |
| HTTP | cc.http() | No | Forward events to an external webhook |
| Prompt | cc.prompt() | No | LLM single-turn evaluation (PreToolUse/PostToolUse/PermissionRequest) |
| Agent | cc.agent() | No | LLM multi-turn verification (PreToolUse/PostToolUse/PermissionRequest) |
Output Helpers
| Helper | Effect | Events |
| -------------------- | -------------------------------------- | ------------------------------ |
| deny(reason?) | Block tool / deny permission | PreToolUse, PermissionRequest |
| allow(reason?) | Auto-approve tool / grant permission | PreToolUse, PermissionRequest |
| ask(reason?) | Prompt user for confirmation | PreToolUse |
| addContext(text) | Inject text into Claude's conversation | Most events |
| modifyInput(input) | Rewrite tool input | PreToolUse, PermissionRequest |
| accept(content?) | Accept elicitation | Elicitation, ElicitationResult |
| decline() | Decline elicitation | Elicitation, ElicitationResult |
| cancel() | Cancel elicitation | Elicitation, ElicitationResult |
Helpers are chainable:
deny("Dangerous").context("See docs for allowed commands")
allow().input({ command: "ls -la" }).context("Modified for safety")
addContext("warning").visible() // macOS notificationExamples
See examples/ for 16 copy-ready extensions covering every hook pattern.
CLI
hx build Build all enabled extensions
hx init Create .claude/extensions/ with a sample extension
hx new <name> Scaffold a new extension
hx list List all extensions and their status
hx activate Toggle extensions on/off (interactive)
hx update Update hx to the latest version
hx clean Remove all hx artifacts
hx completions Generate shell completion scriptsHow It Works
.claude/extensions/my-ext/index.ts # You write this
↓ hx build
.claude/hooks/my-ext.mjs # Compiled hook script (one per extension)
.claude/settings.local.json # Hook entries merged in
↓ Claude Code reads
Hooks fire on tool use, prompts, sessions, etc.- Discover — Find all
.tsfiles in.claude/extensions/ - Collect — Execute the factory to record hook registrations
- Bundle — Compile to a single
.mjsper extension via Bun.build - Merge — Write hook entries into
settings.local.json(hx-managed hooks are tagged and never touch user hooks)
Acknowledgments
This project was inspired by pi-mono by Mario Zechner — particularly the coding-agent extension system.
License
MIT
