Fix Claude Code Hook settings.json Snippet Format
Jira: N/A Date: 2026-06-25
Problem
The aiot install command prints a settings.json snippet for users to paste into ~/.claude/settings.json. The snippet is generated by renderSnippet in apps/hook/src/adapters/claude-code.ts.
The generated snippet is invalid — Claude Code does not recognize it:
{ "hooks": { "session-start": "/path/to/aiot hook session-start", "pre-tool-use": "/path/to/aiot hook pre-tool-use", ... }}Two bugs:
- Wrong event name casing — Claude Code requires PascalCase keys (
SessionStart,PreToolUse, …). The snippet uses kebab-case (session-start,pre-tool-use, …). Unrecognized keys are silently ignored, so no hooks fire. - Wrong value type — Each key’s value must be an array of hook-group objects
[{ hooks: [{ type: "command", command: "..." }] }], not a plain string.
Because both bugs are present, the current install output produces a settings.json that registers zero hooks. The binary is silently never invoked by Claude Code.
Approach
Fix renderSnippet in apps/hook/src/adapters/claude-code.ts to produce the correct format. Add a unit test asserting the exact structure.
The CLI arg names (hook session-start, hook pre-tool-use, etc.) do not change — they are internal to the binary and do not need to match Claude Code’s event naming convention.
Design
Correct settings.json format (per official Claude Code docs)
{ "hooks": { "SessionStart": [{ "hooks": [{ "type": "command", "command": "/path/to/binary hook session-start" }] }], "PreToolUse": [{ "hooks": [{ "type": "command", "command": "/path/to/binary hook pre-tool-use" }] }], "PostToolUse": [{ "hooks": [{ "type": "command", "command": "/path/to/binary hook post-tool-use" }] }], "Stop": [{ "hooks": [{ "type": "command", "command": "/path/to/binary hook stop" }] }], "UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "/path/to/binary hook user-prompt-submit" }] }], "PreCompact": [{ "hooks": [{ "type": "command", "command": "/path/to/binary hook pre-compact" }] }], "SubagentStop": [{ "hooks": [{ "type": "command", "command": "/path/to/binary hook subagent-stop" }] }], "Notification": [{ "hooks": [{ "type": "command", "command": "/path/to/binary hook notification" }] }] }}Changes
apps/hook/src/adapters/claude-code.ts
- Add a
HOOK_KIND_TO_EVENT_NAMEconstant mapping eachHookKind(kebab-case CLI arg) to its PascalCase Claude Code event name. - Rewrite
renderSnippetto iterate overHOOK_KINDS, look up the PascalCase name, and build the correct nested structure. - No changes to
HOOK_KINDS,isHookKind,mapPayload,transcriptTarget, orinstallConfig(thesettingsHinttext remains accurate).
apps/hook/src/adapters/claude-code.test.ts (new file)
- Test that
renderSnippetproduces valid JSON with PascalCase keys and the correct[{hooks:[{type:"command",command:"..."}]}]structure for every hook kind.
No-change scope
- CLI arg names (
session-start,pre-tool-use, …) — unchanged. payload.tsHOOK_KIND_TO_EVENT_TYPE— unchanged and not imported here (the adapter keeps its Claude Code-specific knowledge local).- Other adapters (
opencode.ts,codex.ts) — untouched; they emit different config formats. install.ts— no changes needed; it already callsadapter.installConfig().renderSnippet(bin).
Acceptance Criteria
- When
renderSnippet(bin)is called with any non-empty binary path, the system shall return valid JSON whose top-level key is"hooks". - When the parsed
hooksobject is inspected, the system shall contain exactly 8 keys:SessionStart,PreToolUse,PostToolUse,Stop,UserPromptSubmit,PreCompact,SubagentStop,Notification— no kebab-case keys. - For each key, the system shall have a value that is an array of length 1 where the single element has a
hooksarray of length 1 containing{ type: "command", command: "<bin> hook <kind>" }. - When
aiot installis run and output is copy-pasted into~/.claude/settings.json, Claude Code shall invoke the binary for each registered lifecycle event.
Out of Scope
- Adding new hook event types beyond the current 8 (e.g.,
SessionEnd,SubagentStart,PostCompact) — tracked separately. - Changes to how the install command loads/unloads launchd/systemd services.
- Changes to other adapters.
Risks & Open Questions
- None. The fix is a pure output-format change with no effect on event processing, queuing, or flushing. Existing installations need to be manually updated (paste the new snippet), but that’s acceptable for a pre-GA tool.
Testing
- Unit test in
apps/hook/src/adapters/claude-code.test.tscovering the exact JSON structure ofrenderSnippet. - Manual verification: run
./dist/aiot installand confirm the snippet parses correctly.