Symptom
A freshly-onboarded consumer repo runs PostToolUse(lint), Stop(test_affected) and PreToolUse(guard-git-add) twice per turn.
Observed in reDeploy as ● Ran 2 stop hooks, with one of the two failing as:
Stop hook error: Failed with non-blocking status code: No stderr output
Two concurrent pnpm -r build into the same dist/ — one lost the race and exited non-zero having written only to stdout, hence the empty-stderr report.
Cause
Both of these register the same three hooks:
.claude/skills/setup/templates/settings.json — scaffolded into the consumer as .claude/settings.json
.claude/hooks/hooks.json — shipped by the plugin and loaded whenever it's enabled
Since the plugin must stay enabled for everyday sessions (#134), both fire.
log-worker-tool.sh is the one entry that is genuinely template-only, so the template is not wholly redundant.
Why the existing check doesn't catch it
do_hooks_parity (#140) compares this repo's own .claude/settings.json against .claude/hooks/hooks.json, and its allowlist comment frames the distinction as "self-hosted repo" vs "shipped to consumers". It never looks at skills/setup/templates/settings.json, which is the file consumers actually receive. So parity passes while consumers double-run.
The design question (why this isn't just a delete)
Removing the three duplicated hooks from the template is only correct if the plugin is guaranteed enabled. The template's ${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude} fallback reads like a deliberate plugin-disabled escape hatch — if that's the intent, the fix is different (e.g. keep the template hooks and drop hooks.json, or make one conditional). That call is yours, which is why this is an issue rather than a PR.
Worth noting the same tension appears in the other direction: the parity allowlist says log-worker-tool.sh "must never be added to hooks.json" because consumers have no cockpit to feed — yet the consumer-facing template does include it.
Impact
Every gate runs twice per turn in every onboarded repo: double CPU, double wall-clock, and a real race between two builds writing the same output tree. It also doubles the cost of claude remote-control planner sessions, which load the same project hooks via WorkingDirectory=<repo>.
Repro
reDeploy at plugin 0.3.0. .claude/settings.json (scaffolded) + plugin hooks.json both present → Ran 2 stop hooks.
Filed from reDeploy per #223. Labelled backlog — not self-assigning planned, per CONTRIBUTING.md.
Symptom
A freshly-onboarded consumer repo runs
PostToolUse(lint),Stop(test_affected)andPreToolUse(guard-git-add)twice per turn.Observed in reDeploy as
● Ran 2 stop hooks, with one of the two failing as:Two concurrent
pnpm -r buildinto the samedist/— one lost the race and exited non-zero having written only to stdout, hence the empty-stderr report.Cause
Both of these register the same three hooks:
.claude/skills/setup/templates/settings.json— scaffolded into the consumer as.claude/settings.json.claude/hooks/hooks.json— shipped by the plugin and loaded whenever it's enabledSince the plugin must stay enabled for everyday sessions (#134), both fire.
log-worker-tool.shis the one entry that is genuinely template-only, so the template is not wholly redundant.Why the existing check doesn't catch it
do_hooks_parity(#140) compares this repo's own.claude/settings.jsonagainst.claude/hooks/hooks.json, and its allowlist comment frames the distinction as "self-hosted repo" vs "shipped to consumers". It never looks atskills/setup/templates/settings.json, which is the file consumers actually receive. So parity passes while consumers double-run.The design question (why this isn't just a delete)
Removing the three duplicated hooks from the template is only correct if the plugin is guaranteed enabled. The template's
${CLAUDE_PLUGIN_ROOT:-$CLAUDE_PROJECT_DIR/.claude}fallback reads like a deliberate plugin-disabled escape hatch — if that's the intent, the fix is different (e.g. keep the template hooks and drophooks.json, or make one conditional). That call is yours, which is why this is an issue rather than a PR.Worth noting the same tension appears in the other direction: the parity allowlist says
log-worker-tool.sh"must never be added to hooks.json" because consumers have no cockpit to feed — yet the consumer-facing template does include it.Impact
Every gate runs twice per turn in every onboarded repo: double CPU, double wall-clock, and a real race between two builds writing the same output tree. It also doubles the cost of
claude remote-controlplanner sessions, which load the same project hooks viaWorkingDirectory=<repo>.Repro
reDeploy at plugin 0.3.0.
.claude/settings.json(scaffolded) + pluginhooks.jsonboth present →Ran 2 stop hooks.Filed from reDeploy per #223. Labelled
backlog— not self-assigningplanned, per CONTRIBUTING.md.