Skip to content

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:

  1. 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.
  2. 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_NAME constant mapping each HookKind (kebab-case CLI arg) to its PascalCase Claude Code event name.
  • Rewrite renderSnippet to iterate over HOOK_KINDS, look up the PascalCase name, and build the correct nested structure.
  • No changes to HOOK_KINDS, isHookKind, mapPayload, transcriptTarget, or installConfig (the settingsHint text remains accurate).

apps/hook/src/adapters/claude-code.test.ts (new file)

  • Test that renderSnippet produces 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.ts HOOK_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 calls adapter.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 hooks object 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 hooks array of length 1 containing { type: "command", command: "<bin> hook <kind>" }.
  • When aiot install is 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.ts covering the exact JSON structure of renderSnippet.
  • Manual verification: run ./dist/aiot install and confirm the snippet parses correctly.