From 846f0deb1b733c4982ea3ab2644fe09e47322d1a Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Fri, 24 Apr 2026 11:19:23 +0200 Subject: [PATCH] Prevent Claude edit stalls from missing reads Claude Code rejects edits to existing files when the file has not been read in the current session. Surface that rule in the Claude playbook and inject a short UserPromptSubmit reminder only for claude-code sessions so Update/Edit/MultiEdit attempts read their target first instead of stalling mid-task. Constraint: Claude Code enforces read-before-edit outside Colony, so the fix must guide the agent before it plans edit tools. Rejected: Treat this as a Colony hook error | the failing message is emitted by Claude Code before PostToolUse can record an edit. Confidence: high Scope-risk: narrow Directive: Keep this reminder Claude-only; do not add it to Codex or other IDE contexts unless their edit tools need the same guard. Tested: pnpm --filter @colony/hooks test; pnpm --filter @colony/hooks build; pnpm exec biome check CLAUDE.md packages/hooks/src/handlers/user-prompt-submit.ts packages/hooks/test/runner.test.ts .changeset/claude-edit-read-guard.md Not-tested: pnpm --filter @colony/hooks typecheck is blocked by existing missing declaration for better-sqlite3 in @colony/storage. --- .changeset/claude-edit-read-guard.md | 5 ++++ CLAUDE.md | 3 ++- .../hooks/src/handlers/user-prompt-submit.ts | 5 +++- packages/hooks/test/runner.test.ts | 27 +++++++++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 .changeset/claude-edit-read-guard.md diff --git a/.changeset/claude-edit-read-guard.md b/.changeset/claude-edit-read-guard.md new file mode 100644 index 0000000..d10d4ab --- /dev/null +++ b/.changeset/claude-edit-read-guard.md @@ -0,0 +1,5 @@ +--- +"@colony/hooks": patch +--- + +Remind Claude Code to read existing files before edit tools so Update/Edit/MultiEdit calls do not stop with "File must be read first". diff --git a/CLAUDE.md b/CLAUDE.md index 89f7b0c..e1ebfa0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,7 +20,8 @@ The signature property of the project is that **memory is stored compressed**. E 8. **Local by default.** Default embedding provider is local (Transformers.js). Remote providers are opt-in via settings. Do not add default network calls. 9. **No silent failures.** Hook and worker errors are logged as structured JSON; user-visible commands surface failures with a non-zero exit code and a short message. 10. **No daemon on the write path.** Hooks write observations synchronously through `MemoryStore.addObservation` — never across a network or HTTP boundary. Hooks may *detach-spawn* the worker to kick off background embedding, but they must never wait on it. If the worker is down, writes still succeed; only the semantic-search side is degraded (BM25 keeps working). -11. **Never edit on the local base branch.** Treat the local `main` checkout as read-only. Every task — even a typo or one-line fix — runs on a dedicated `agent/*` branch inside a worktree. Do not run `git checkout main` / `git switch main` to start work, do not `git commit` on the primary working tree, and do not push to `main` directly. This matches what codex does via Guardex and keeps parallel lanes safe. +11. **Read before edit tools.** Claude Code rejects `Edit` / `Update` / `MultiEdit` on an existing file unless that exact file path was read first in the current session. Before any edit tool call, run `Read` on the target file with the same relative path you will edit. +12. **Never edit on the local base branch.** Treat the local `main` checkout as read-only. Every task — even a typo or one-line fix — runs on a dedicated `agent/*` branch inside a worktree. Do not run `git checkout main` / `git switch main` to start work, do not `git commit` on the primary working tree, and do not push to `main` directly. This matches what codex does via Guardex and keeps parallel lanes safe. ## Worktree discipline diff --git a/packages/hooks/src/handlers/user-prompt-submit.ts b/packages/hooks/src/handlers/user-prompt-submit.ts index ef1d8f1..720980a 100644 --- a/packages/hooks/src/handlers/user-prompt-submit.ts +++ b/packages/hooks/src/handlers/user-prompt-submit.ts @@ -7,6 +7,8 @@ import type { HookInput } from '../types.js'; * so one chatty agent can't drown out the signal in another agent's context. */ const MAX_INJECTED_MESSAGES = 6; +const CLAUDE_EDIT_READ_GUARD = + 'Claude Code edit safety: Read each existing target file before Edit/Update/MultiEdit, or Claude Code rejects with "File must be read first".'; export async function userPromptSubmit(store: MemoryStore, input: HookInput): Promise { // Compute the "since" cursor *before* recording this turn's prompt so the @@ -24,7 +26,8 @@ export async function userPromptSubmit(store: MemoryStore, input: HookInput): Pr const activity = buildTaskUpdatesPreface(store, input.session_id, sinceTs); const conflicts = buildConflictPreface(store, input.session_id); const pheromones = buildPheromoneConflictPreface(store, input.session_id); - return [activity, conflicts, pheromones].filter(Boolean).join('\n\n'); + const editGuard = input.ide === 'claude-code' ? CLAUDE_EDIT_READ_GUARD : ''; + return [editGuard, activity, conflicts, pheromones].filter(Boolean).join('\n\n'); } /** diff --git a/packages/hooks/test/runner.test.ts b/packages/hooks/test/runner.test.ts index 1d04596..bc9471f 100644 --- a/packages/hooks/test/runner.test.ts +++ b/packages/hooks/test/runner.test.ts @@ -52,6 +52,33 @@ describe('runHook', () => { expect(tl[0]?.content).not.toMatch(/basically/i); }); + it('user-prompt-submit reminds Claude Code to read files before edit tools', async () => { + const claude = await runHook( + 'user-prompt-submit', + { + session_id: 'sess-claude-edit', + ide: 'claude-code', + prompt: 'Update packages/hooks/src/handlers/user-prompt-submit.ts', + }, + { store }, + ); + expect(claude.ok).toBe(true); + expect(claude.context).toContain('Read each existing target file before Edit/Update/MultiEdit'); + expect(claude.context).toContain('File must be read first'); + + const codex = await runHook( + 'user-prompt-submit', + { + session_id: 'sess-codex-edit', + ide: 'codex', + prompt: 'Update packages/hooks/src/handlers/user-prompt-submit.ts', + }, + { store }, + ); + expect(codex.ok).toBe(true); + expect(codex.context ?? '').not.toContain('Edit/Update/MultiEdit'); + }); + it('post-tool-use records a tool_use observation with metadata', async () => { await runHook('session-start', { session_id: 'sess-c', ide: 'claude-code' }, { store }); const r = await runHook(