Problem
ai/scripts/setup/initServerConfigs.mjs gained initClaudeSettings() (+ mergeClaudeHooks()): it materializes the gitignored .claude/settings.json from the tracked .claude/settings.template.json, wiring the Claude Stop hook (NEO_LANE_STATE_ENFORCE=1 … laneStateStopHook.mjs — the no-hold lane-state enforcement). It runs at npm prepare, the Claude analog of initConfigs / initTier1Config.
ai/scripts/migrations/bootstrapWorktree.mjs (the worktree-hydration script) hydrates BOOTSTRAP_CONFIGS — ai/config.mjs + each ai/mcp/server/*/config.mjs (the initTier1Config / initConfigs parallel) — but never calls initClaudeSettings. So a worktree's .claude/settings.json is only wired by the npm prepare that installDependencies skips when node_modules already exists (the common case for an established worktree). Result: the Stop hook is unwired in worktrees — the no-hold enforcement silently doesn't fire there.
Evidence (V-B-A)
- This worktree's
.claude/settings.json (Jun 20 18:02) predated the hook-wiring → no Stop hook. The hook never fired for @neo-opus-ada ("the last maintainer who hadn't tried it").
- Running
initClaudeSettings({claudeDir}) manually on the worktree wired the ENFORCE Stop hook ({"action":"wired"}) — confirming the fix shape works end-to-end in a worktree.
Fix
bootstrapWorktree.mjs:
- Import
initClaudeSettings from ../setup/initServerConfigs.mjs (already imports listServersWithTemplates + materializeServerConfigTemplate from there).
- Call
initClaudeSettings({claudeDir: path.join(projectRoot, '.claude')}) in the CLI hydration flow (parallel to the BOOTSTRAP_CONFIGS copy + symlinkDataDir) and in hydrateCurrentWorktree() (the --prune-stale rehydrate path).
Deterministic — wires the hook regardless of whether npm install (→ npm prepare) runs.
Design note (Tier-2, decide-and-document)
Materialize from the worktree's own tracked template (initClaudeSettings) rather than copy main's settings.json (the BOOTSTRAP_CONFIGS mechanism). Rationale: matches the initServerConfigs parallel + the ENFORCE-rollout intent (worktrees get the hook on by default). The per-checkout opt-out (drop the NEO_LANE_STATE_ENFORCE=1 prefix in the gitignored settings.json) stays a deliberate operator choice, not silently inherited. In the common case the two are identical (main's settings.json is itself materialized from the same template).
Acceptance Criteria
Related
ai/scripts/setup/initServerConfigs.mjs — initClaudeSettings / mergeClaudeHooks (the parallel this mirrors).
- #13652 — [Epic] Mechanical enforcement replaces prompt-machinery (the hook rollout).
- The
bootstrapWorktree-is-canonical worktree-config-hydration discipline (never hand-cp).
Problem
ai/scripts/setup/initServerConfigs.mjsgainedinitClaudeSettings()(+mergeClaudeHooks()): it materializes the gitignored.claude/settings.jsonfrom the tracked.claude/settings.template.json, wiring the Claude Stop hook (NEO_LANE_STATE_ENFORCE=1 … laneStateStopHook.mjs— the no-hold lane-state enforcement). It runs atnpm prepare, the Claude analog ofinitConfigs/initTier1Config.ai/scripts/migrations/bootstrapWorktree.mjs(the worktree-hydration script) hydratesBOOTSTRAP_CONFIGS—ai/config.mjs+ eachai/mcp/server/*/config.mjs(theinitTier1Config/initConfigsparallel) — but never callsinitClaudeSettings. So a worktree's.claude/settings.jsonis only wired by thenpm preparethatinstallDependenciesskips whennode_modulesalready exists (the common case for an established worktree). Result: the Stop hook is unwired in worktrees — the no-hold enforcement silently doesn't fire there.Evidence (V-B-A)
.claude/settings.json(Jun 20 18:02) predated the hook-wiring → no Stop hook. The hook never fired for@neo-opus-ada("the last maintainer who hadn't tried it").initClaudeSettings({claudeDir})manually on the worktree wired the ENFORCE Stop hook ({"action":"wired"}) — confirming the fix shape works end-to-end in a worktree.Fix
bootstrapWorktree.mjs:initClaudeSettingsfrom../setup/initServerConfigs.mjs(already importslistServersWithTemplates+materializeServerConfigTemplatefrom there).initClaudeSettings({claudeDir: path.join(projectRoot, '.claude')})in the CLI hydration flow (parallel to theBOOTSTRAP_CONFIGScopy +symlinkDataDir) and inhydrateCurrentWorktree()(the--prune-stalerehydrate path).Deterministic — wires the hook regardless of whether
npm install(→npm prepare) runs.Design note (Tier-2, decide-and-document)
Materialize from the worktree's own tracked template (
initClaudeSettings) rather than copymain'ssettings.json(theBOOTSTRAP_CONFIGSmechanism). Rationale: matches theinitServerConfigsparallel + the ENFORCE-rollout intent (worktrees get the hook on by default). The per-checkout opt-out (drop theNEO_LANE_STATE_ENFORCE=1prefix in the gitignoredsettings.json) stays a deliberate operator choice, not silently inherited. In the common case the two are identical (main'ssettings.jsonis itself materialized from the same template).Acceptance Criteria
bootstrapWorktreematerializes/ensures.claude/settings.json(the Stop hook) in a worktree on bootstrap.mergeClaudeHooks'changed:false).hydrateCurrentWorktree(the prune-rehydrate path) also wires it.Related
ai/scripts/setup/initServerConfigs.mjs—initClaudeSettings/mergeClaudeHooks(the parallel this mirrors).bootstrapWorktree-is-canonical worktree-config-hydration discipline (never hand-cp).