Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/claude-edit-read-guard.md
Original file line number Diff line number Diff line change
@@ -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".
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 4 additions & 1 deletion packages/hooks/src/handlers/user-prompt-submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
// Compute the "since" cursor *before* recording this turn's prompt so the
Expand All @@ -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');
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/hooks/test/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading