Skip to content

fix(cli): atomic hook state writes, SDK fetch timeout, correct git cwd - #25

Merged
iaj6 merged 1 commit into
mainfrom
fix/hook-hardening
Jul 12, 2026
Merged

iaj6 merged 1 commit into
mainfrom
fix/hook-hardening

Conversation

@iaj6

@iaj6 iaj6 commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

Hook-pipeline hardening from the July audit's medium tier:

  • Atomic state writes — writeState now writes a pid-suffixed temp file and renameSyncs it into place (mirroring the outbox pattern), so parallel-subagent hooks can never observe a torn state file (a torn read silently skipped policy enforcement for that tool call). 0600/0700 modes preserved; temp cleaned up on rename failure.
  • SDK fetch timeout — every dashboard fetch now carries AbortSignal.timeout(...), default 5s, overridable via AGENTOPS_SDK_TIMEOUT_MS. A hanging (not refusing) dashboard used to stall every PreToolUse for Claude Code's full 60s hook timeout. Timeouts flow through the existing status-0 SdkError path, so fail-open/fail-closed and outbox semantics are unchanged.
  • Correct git diff cwd — getChangedFiles/getWorkingTreeDiff accept a cwd and finalizeSession passes the session's; previously a Claude Code session launched from outside the tracked repo recorded another repo's uncommitted changes as the run's diff.
  • Dead code removed — getDiff/getCommitLog/snapshotRef (zero callers, shell-interpolated execSync args — latent injection) and handleStaleState (call sites removed in the Phase 3.4 HookOps refactor; DirectOps/SdkOps no longer observe run existence, so there is no sensible call site).

Tests

New suites: atomic-write behavior (no temp leftovers, no torn content, cleanup on rename failure), timeout (env parsing, every fetch has a signal, hanging-server stub → fail-open + outbox), git cwd threading, and an end-to-end hook test against a real throwaway git repo asserting the run diff comes from the session's cwd (fails pre-fix). Full workspace suite green (1,294 tests).

🤖 Generated with Claude Code

Hardens the Claude Code hook pipeline per the July 2026 audit:

- writeState now writes to a pid-suffixed temp file and renameSync's it
  into place (atomic on POSIX, same pattern as Outbox.drain). Prevents
  parallel-subagent hooks from reading a torn state file, which made
  readState return null and silently skipped policy enforcement for
  that tool call. 0600 mode preserved; temp file cleaned up on failure.

- Every SdkOps fetch now carries AbortSignal.timeout(sdkTimeoutMs()) so
  a hanging (rather than refusing) dashboard can't stall checkPolicy on
  each PreToolUse until Claude Code's 60s hook timeout. Default 5s,
  overridable via AGENTOPS_SDK_TIMEOUT_MS. Timeouts surface through the
  existing network-error path (status 0 -> transient SdkError): reports
  are outboxed, policy checks fail-open (or block under
  AGENTOPS_FAIL_CLOSED).

- finalizeSession now passes state.cwd ?? input.cwd into
  getChangedFiles/getWorkingTreeDiff (both grew an optional cwd param),
  matching the cwd threading session-start already did. Previously the
  hook subprocess diffed whatever directory Claude Code was launched
  from, recording another repo's changes on the run.

- Deleted dead, shell-interpolating git helpers getDiff, getCommitLog,
  snapshotRef (zero callers outside their own tests) and their tests.

- Deleted dead handleStaleState: its call sites (getRun-returns-null
  after a DB reset) were removed when the HookOps abstraction landed in
  Phase 3.4, and the current handler layer never sees run existence, so
  there is no sensible call site without widening the HookOps contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@iaj6
iaj6 merged commit 6846250 into main Jul 12, 2026
3 checks passed
@iaj6
iaj6 deleted the fix/hook-hardening branch July 12, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant