* Edit safety: file version tracking, conflict detection, hook wiring
Phase 1 — FileTracker (internal/tools/file_tracker.go): per-session
map[absPath]{sha256, size, mtime}; CheckConflict flags drift only against
a known baseline, so first-touch writes are unaffected. nil = no-op.
Phase 2 — external-modification detection: read_file records the
whole-file baseline; edit_file and write_file --overwrite refuse to
clobber a file whose on-disk content drifted from it and tell the model
to re-read first, then re-baseline to what they wrote. apply_patch
forgets the files it rewrote (git apply has its own context guard).
Threaded via RunOptions/agent.Options FileTracker, built once per session
in the exec, spec, and TUI paths.
Phase 3 — post-apply validation via hooks: the dormant hooks.Dispatcher
is wired into executeToolCall. beforeTool hooks can veto a call
(DenialHookBlocked); afterTool hook output is scrubbed and surfaced to
the model. DispatchOutcome now collects hook stdout/stderr. Per-session
dispatcher built from merged user+project hooks.json; fails open and runs
nothing until a hook is configured.
* edit-safety: address review — fail closed on read, redact hook reason
- write_file overwrite: if a tracked file cannot be re-read to verify it,
fail CLOSED (refuse the overwrite) instead of silently proceeding — the
read error previously skipped the conflict check and could clobber an
externally modified or replaced file.
- blockedByHookResult: scrub outcome.Reason through redaction before placing
it in model-visible ToolResult.Output; this intercepted path bypassed the
registry's normal output-redaction boundary.
- TestHashContentIsStableAndDistinguishing: compare separately stored hashes
so the stability check is not a same-expression comparison (SA4000).
* edit-safety: flag ToolResult.Redacted when hook output is scrubbed
appendHookFeedback now reports whether scrubbing changed the afterTool
feedback, and blockedByHookResult reports whether it scrubbed the reason;
both set ToolResult.Redacted so the hook-intercepted paths match the
registry's redaction contract. Adds tests asserting a secret-bearing hook
reason/feedback is scrubbed and flips Redacted.
Fixes from the code review on the critical/high audit branch:
- tools/apply_patch: parseHunkCounts now reads only the range section between
the opening and closing "@@". A crafted section heading like
"@@ -1,1 +1,1 @@ +1,999999" could otherwise overwrite the line count, wedge
the parser in hunk mode, and swallow later file headers so they escaped
validatePatchPaths / recheckPatchWriteTargets — a workspace-confinement bypass.
- providerhealth: the SSRF-hardened dial now tries every validated address in
order instead of pinning addrs[0], so a dual-stack/multi-record provider isn't
failed by a single dead record. Extracted dialValidatedAddrs for coverage.
- hooks/dispatch: a beforeTool hook killed by its deadline now fails CLOSED
(vetoes the tool) instead of fail-open. A launch failure (missing binary)
still fails open so a misconfigured hook can't wedge every tool call.
- cron: Store.Get returns a distinct ErrJobNotFound sentinel; cron_run only
treats that as "removed during run" and still records/persists on a transient
read error instead of silently dropping the run.
- providers/anthropic: closeOpen finalizes thinking buffers still open at stream
end (SSE ended after thinking_delta/signature_delta but before
content_block_stop), so they survive in the done event's ReasoningBlocks for
replay. Finalized in index order.
- providers/gemini: corrected the stale thinkingConfig comment (no IncludeThoughts
field; thought summaries are suppressed by skipping part.Thought parts).
Tests: regression coverage for each fix, plus the requested nitpicks —
EOF-terminated MCP line, recipe-prompt propagation assertion, checked
file.Close() in session store tests, and a shortened bash process-group timeout
test via the bashWaitDelay hook.
Note: declined the suggestion to replay a "signature" on redacted_thinking
blocks — Anthropic's redacted_thinking carries only an opaque "data" field; the
signature is for regular thinking blocks, and the stream never populates one for
redacted blocks.
The package had all the primitives (Select, ConfigStore, AuditStore) but no
caller ever ran a hook. Add a Dispatcher that selects the hooks for a lifecycle
event, runs each command (no shell) with the event payload on stdin under a
timeout, and records started/completed audit events. A beforeTool hook that
exits non-zero vetoes the tool and stops the chain; other events are advisory.
A hook that cannot be launched is recorded as an error but never blocks. The
command runner is injectable for testing.