{"_id":"@aeriondyseti/hook-kit","name":"@aeriondyseti/hook-kit","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@aeriondyseti/hook-kit","version":"1.0.0","description":"Ergonomic helpers for writing Claude Code hook scripts.","type":"module","repository":{"type":"git","url":"git+https://github.com/aeriondyseti/hook-kit.git"},"homepage":"https://github.com/aeriondyseti/hook-kit#readme","bugs":{"url":"https://github.com/aeriondyseti/hook-kit/issues"},"publishConfig":{"access":"public"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./testing":{"types":"./dist/testing.d.ts","import":"./dist/testing.js"}},"license":"MIT","keywords":["claude","claude-code","hook","hooks","cli","anthropic"],"engines":{"node":">=20"},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest"},"dependencies":{"string-width":"^7.2.0"},"devDependencies":{"@types/node":"^20.14.0","tsup":"^8.2.0","typescript":"^5.6.0","vitest":"^2.1.0"},"gitHead":"d7e6f66d27ee70c386075ff9d34c6b2b980a629b","_id":"@aeriondyseti/hook-kit@1.0.0","_nodeVersion":"25.2.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-+axIrXZa/EeZ62KmaLb39IGfi6wLOHS8y9Ac8Tpg9u16EZT+hYK3tiHubr6YAZ6m5rRvKdfoIQ+GyZMCwHzNHA==","shasum":"cc5be72a0226895fc35f3d380d35e326ce6dc21b","tarball":"https://registry.npmjs.org/@aeriondyseti/hook-kit/-/hook-kit-1.0.0.tgz","fileCount":27,"unpackedSize":131369,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIED5KdHez7+CnSsJ1Wj6Wbc1mQhgWeF3+vcYGLDDp6OpAiEAyhQQf4oP4M8NJtLzO1FfG8E4ZHtB0qgIAFgGnJKZNoo="}]},"_npmUser":{"name":"aeriondyseti","email":"inblessedsilencewaiting@gmail.com"},"directories":{},"maintainers":[{"name":"aeriondyseti","email":"inblessedsilencewaiting@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/hook-kit_1.0.0_1776873877494_0.6726399852503637"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-22T16:04:37.395Z","1.0.0":"2026-04-22T16:04:37.646Z","modified":"2026-04-22T16:04:37.853Z"},"maintainers":[{"name":"aeriondyseti","email":"inblessedsilencewaiting@gmail.com"}],"description":"Ergonomic helpers for writing Claude Code hook scripts.","homepage":"https://github.com/aeriondyseti/hook-kit#readme","keywords":["claude","claude-code","hook","hooks","cli","anthropic"],"repository":{"type":"git","url":"git+https://github.com/aeriondyseti/hook-kit.git"},"bugs":{"url":"https://github.com/aeriondyseti/hook-kit/issues"},"license":"MIT","readme":"# @aeriondyseti/hook-kit\n\nErgonomic, typed helpers for writing [Claude Code](https://docs.claude.com/en/docs/claude-code) hook scripts.\n\n- One class per hook event with two static methods: `parse()` reads and\n  validates stdin, `emitOutput()` writes the response JSON and exits.\n- A small `OutputBuilder` for styled multi-line text — boxes, tables, lists,\n  dividers, icons, colors via tag markup.\n- A testing subpath (`@aeriondyseti/hook-kit/testing`) with `testHook`,\n  `mockXxx` input factories, and normalized result fields so you can assert\n  `result.wasDenied` instead of spelunking the payload.\n\n## Install\n\n```bash\nnpm install @aeriondyseti/hook-kit\n```\n\nRequires Node 20+. ESM-only.\n\n## A minimal hook\n\n```ts\n#!/usr/bin/env node\nimport { PreToolUse, runHook } from '@aeriondyseti/hook-kit';\n\nrunHook(() => {\n    const input = PreToolUse.parse();\n    const cmd = String((input.tool_input as { command?: unknown }).command ?? '');\n\n    if (/\\brm\\b.*-rf?\\s+\\//.test(cmd)) {\n        PreToolUse.emitOutput({\n            decision: 'deny',\n            reason: 'Refusing dangerous rm on the filesystem root.',\n        });\n    }\n\n    PreToolUse.emitOutput({});\n});\n```\n\nWire it up in your `settings.json`:\n\n```json\n{\n    \"hooks\": {\n        \"PreToolUse\": [\n            { \"matcher\": \"Bash\", \"hooks\": [{ \"type\": \"command\", \"command\": \"node /path/to/pre-tool-use.ts\" }] }\n        ]\n    }\n}\n```\n\n## Styled output\n\n```ts\nimport { ICONS, OutputBuilder, PostToolUse } from '@aeriondyseti/hook-kit';\n\nconst toUser = new OutputBuilder()\n    .appendBox(`${ICONS.check} ${input.tool_name}`, { title: '● PostToolUse', color: 'green' })\n    .appendTable(rows, { headers: ['key', 'value'], color: 'green' });\n\nPostToolUse.emitOutput({ toUser });\n```\n\nColors and modifiers also work via inline tags:\n\n```ts\nbuilder.appendLine('<color:\"red\"><bold>boom</bold></color>');\n```\n\nAvailable icons: `check cross warn info arrow bullet dot star`.\nAvailable colors: `black red green yellow blue magenta cyan white gray`.\nAvailable modifiers: `bold dim italic underline`.\n\n## Testing your hooks\n\n```ts\nimport { describe, expect, it } from 'vitest';\nimport { PreToolUse } from '@aeriondyseti/hook-kit';\nimport { mockPreToolUse, testHook } from '@aeriondyseti/hook-kit/testing';\nimport { handle } from './pre-tool-use.js';\n\nit('denies rm -rf', () => {\n    const result = testHook(\n        mockPreToolUse({ tool_name: 'Bash', tool_input: { command: 'rm -rf /' } }),\n        () => handle(PreToolUse.parse()),\n    );\n    expect(result.wasDenied).toBe(true);\n    expect(result.toClaude).toContain('rm');\n});\n```\n\n`TestHookResult` carries normalized fields so you don't have to walk the\npayload yourself:\n\n| Field          | Meaning                                                                    |\n| -------------- | -------------------------------------------------------------------------- |\n| `wasDenied`    | `permissionDecision === 'deny'` or top-level `decision === 'block'`        |\n| `wasAllowed`   | explicit allow, or no blocking/ask signal at all                           |\n| `wasAsked`     | `permissionDecision === 'ask'`                                             |\n| `toUser`       | `payload.systemMessage`                                                    |\n| `toClaude`     | `additionalContext` → `permissionDecisionReason` → top-level `reason`      |\n\nNegative paths (malformed stdin, wrong `hook_event_name`) surface as a\nthrown `HookParseError`:\n\n```ts\nexpect(() => testHook(wrongEvent, () => PreToolUse.parse())).toThrow(HookParseError);\n```\n\n## Examples\n\nRunnable dogfood hooks with colocated tests live in\n[`examples/hooks/`](examples/hooks). Each hook exports a pure `handle(input)`\nfunction so its policy is testable without touching stdin, with an\n`import.meta.url` guard that drives the real parse/emit only when run as a\nscript.\n\n| Hook                                                             | Shows                                                       |\n| ---------------------------------------------------------------- | ----------------------------------------------------------- |\n| [`pre-tool-use.ts`](examples/hooks/pre-tool-use.ts)              | Deny branch, `OutputBuilder` with box + table, icons        |\n| [`pre-tool-use-advanced.ts`](examples/hooks/pre-tool-use-advanced.ts) | `decision: 'ask'` and `updatedInput` rewrites               |\n| [`post-tool-use.ts`](examples/hooks/post-tool-use.ts)            | Binary `deny: true`, `toClaude` context injection           |\n| [`user-prompt-submit.ts`](examples/hooks/user-prompt-submit.ts)  | Prompt-level deny, divider + list                           |\n| [`session-start.ts`](examples/hooks/session-start.ts)            | Context-injection pattern (no deny concept)                 |\n\n## Project direction\n\n- [`ROADMAP.md`](ROADMAP.md) — features under consideration for future\n  releases.\n- [`CHANGELOG.md`](CHANGELOG.md) — release history, [Keep a Changelog]\n  format.\n- [`TECH-DEBT.md`](TECH-DEBT.md) — known shortcuts and the context behind\n  them, so contributors know what's intentional vs. what's waiting.\n\n## License\n\nMIT.\n\n[Keep a Changelog]: https://keepachangelog.com/en/1.1.0/\n","readmeFilename":"README.md","_rev":"1-11f2b780ca9873d83fe7a4dfee3366f1"}