Skip to content

D57 — Claude Code hooks: automatic registration in settings.json

Context. D48 materialized a hook (hooks/<nome>/.claude/hooks/<nome>/) but stopped there: settings.json was never touched, for fear of duplicating entries in hooks.<Event>[] at every sync (nested arrays, no ownership marker) and of not being able to prune safely — a materialized but dead hook, to be registered by hand.

Decision. For the claude provider only, Apply now automatically registers/updates the entry in <targetDir>/.claude/settings.json right after materializing the hook's files (internal/provisioning/hooksettings.go). Ownership criterion: an entry is "Cartographer's for hook <nome>" if and only if its command contains the substring .claude/hooks/<nome>/ (hookOwnershipMarker) — the materialized path is the signature, no extra field to invent. upsertHookEntry removes every entry with that marker and inserts a fresh one → idempotent. The file is decoded into a map[string]interface{}, not a fixed struct: unknown keys survive, only the key order is not preserved (acceptable: the invariant is "no user data lost"). Prune removes the entry via the same marker when the hook disappears. Rationale. A generic JSON deep-merge has no "obvious" criterion for an array without an identifying key; the per-path marker is targeted and does not require extending the entry format (discarded: a dedicated _cartographer field, risk of breakage with future Claude Code schema checks). Details: docs/sync.md §Agents and hooks.

Update (bare-command bug). resolveHookCommand joined any non-absolute first token (and non-$VAR) to the hook's dir — correct for ./notify.sh, broken for a shell one-liner like jq -e ...: it produced .claude/hooks/<nome>/jq, a nonexistent binary, and the command's || true masked the failure at every invocation. Now the join happens only if the token contains / (relative path); bare names stay verbatim, resolved via PATH as in a shell. Since the ownership marker is the path in the command, a bare command would never contain it (entry neither idempotent nor prunable): for the claude provider only, registerHookSettings appends the marker as an inert shell comment (# cartographer-hook: .claude/hooks/<nome>/) — syntactically neutral, preserves D57's substring criterion without extra fields. Codex/OpenCode do not need it (per-block/per-file ownership).