fix: install Codeman hooks into every claude workspace, not just cases Codeman created

A session in a linked case (or any pre-existing repo) ran with no hooks block
at all: writeHooksConfig only fires when Codeman CREATES the case directory,
and refreshStaleCodemanHooks deliberately never adds one. Every hook-driven
surface was therefore dead in exactly the place most sessions run: no tab
alert or phone-overview NEEDS YOU row when a dialog blocks the pane, no
Approvals Inbox item, no push, no definitive stop/idle_prompt for respawn,
and no stop/blocked for the agent wait endpoints.

Both session-create paths and restoreMuxSessions() now call
ensureCodemanHooks(), an add-only merge that keeps a user's own handlers and
leaves a malformed settings file untouched. Claude Code re-reads
settings.local.json, so a session already running in the workspace starts
firing hooks without a restart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-08-16 04:46:01 +02:00
parent 869a507482
commit 98fa8c00d1
5 changed files with 198 additions and 25 deletions
+34
View File
@@ -76,6 +76,7 @@ import { RunSummaryTracker } from '../run-summary.js';
import { PlanOrchestrator } from '../plan-orchestrator.js';
import { OrchestratorLoop } from '../orchestrator-loop.js';
import { getLifecycleLog } from '../session-lifecycle-log.js';
import { ensureCodemanHooks } from '../hooks-config.js';
import { PushSubscriptionStore } from '../push-store.js';
import webpush from 'web-push';
import { SseStreamManager } from './sse-stream-manager.js';
@@ -2819,6 +2820,13 @@ export class WebServer extends EventEmitter {
}
}
// Sessions recovered from a previous run predate the create-path hook
// install, and these are long-lived: by the time a server restart comes
// round a session may be days old and has been running hook-blind the
// whole time. Claude Code re-reads settings.local.json, so writing the
// block now arms the RUNNING CLI, no session restart needed.
await this.ensureHooksForRecoveredWorkspaces();
// Start stats collection for mux sessions
this.mux.startStatsCollection(STATS_COLLECTION_INTERVAL_MS);
}
@@ -2845,6 +2853,32 @@ export class WebServer extends EventEmitter {
}
}
/**
* Install Codeman's hooks into the workspaces of the sessions just recovered.
*
* Deduped by workspace, because sessions in one repo share a single
* `.claude/settings.local.json` and the write is otherwise repeated per tab.
* Claude mode only (nothing else reads `.claude` hooks), never for remote
* sessions (their `workingDir` is a path on ANOTHER host, so writing it here
* would scaffold a stray directory locally), and never for a docker case that
* opted out of hooks.
*
* Failures are swallowed per workspace: `ensureCodemanHooks` already refuses
* unsafe targets with a warning, and a workspace we cannot write to must not
* stop the rest of recovery.
*/
private async ensureHooksForRecoveredWorkspaces(): Promise<void> {
const workspaces = new Set<string>();
for (const session of this.sessions.values()) {
if (session.mode !== 'claude' || session.remote) continue;
if (session.docker && !session.docker.hooksEnabled) continue;
if (session.workingDir) workspaces.add(session.workingDir);
}
for (const workspace of workspaces) {
await ensureCodemanHooks(workspace).catch(() => {});
}
}
/**
* COD-108 — handle a `remoteSessionDropped` emit from the watcher: reattach
* the dropped remote session and report the outcome back to the watcher so it