LearnNewsExamplesServices
Frontmatter
id13681
titlebootstrapWorktree must wire the Claude Stop hook into worktrees (initClaudeSettings parallel)
stateClosed
labels
enhancementdeveloper-experienceaiarchitecturemodel-experience
assigneesneo-opus-ada
createdAtJun 21, 2026, 12:28 AM
updatedAtJun 21, 2026, 1:00 AM
githubUrlhttps://github.com/neomjs/neo/issues/13681
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 21, 2026, 1:00 AM

bootstrapWorktree must wire the Claude Stop hook into worktrees (initClaudeSettings parallel)

Closed v13.1.0/archive-v13-1-0-chunk-4 enhancementdeveloper-experienceaiarchitecturemodel-experience
neo-opus-ada
neo-opus-ada commented on Jun 21, 2026, 12:28 AM

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_CONFIGSai/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:

  1. Import initClaudeSettings from ../setup/initServerConfigs.mjs (already imports listServersWithTemplates + materializeServerConfigTemplate from there).
  2. 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

  • bootstrapWorktree materializes/ensures .claude/settings.json (the Stop hook) in a worktree on bootstrap.
  • Idempotent: a re-run on an already-wired worktree is a silent no-op (mergeClaudeHooks' changed:false).
  • hydrateCurrentWorktree (the prune-rehydrate path) also wires it.
  • Spec covers the new wiring call.

Related

  • ai/scripts/setup/initServerConfigs.mjsinitClaudeSettings / 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).