Skip to content

fix: fail closed when the audit lock cannot be reclaimed - #96

Merged
safe-agentic-world merged 1 commit into
mainfrom
claude/clever-ride-i2smjt
Sep 26, 2026
Merged

safe-agentic-world merged 1 commit into
mainfrom
claude/clever-ride-i2smjt

Conversation

@safe-agentic-world

Copy link
Copy Markdown
Owner

Problem And Change

Research into keeping the hook authoritative against code the agent runs found a fail-open in the audit writer.

acquireFileLock (internal/audit/filechain.go) treats a lock older than 30 seconds as stale. Its stale-lock branch removed the lock path and then continued without checking the deadline. When os.Remove cannot delete the path, such as a non-empty directory with an old timestamp, the loop spins forever. Then:

  • the hook never answers
  • Claude Code lets a PreToolUse call through when its command hook times out ("A timed-out command, http, or mcp_tool hook doesn't block the tool call")
  • so the call runs with no Nomos decision and no audit record

Code running as the user can plant such a directory at .nomos/claude-code-hook.jsonl.lock. The agent's own tool calls can't, since v0.19.1 protects .nomos/, but a script it runs can.

The change:

  • The deadline now bounds every path through the loop.
  • Only a stale regular file is reclaimed. Lstat is used, so a symlink is never followed or removed.
  • Anything else at the lock path waits out the 3-second deadline like a live lock. The write then fails, and the hook's existing "audit write failed, blocking the tool call" path exits 2, which blocks the call.

Verification

  • TestAcquireFileLockFailsClosedOnUnremovableStaleLock plants a non-empty lock directory with an old timestamp.
    • With this change, it returns an error after the 100 ms deadline.
    • Against the previous filechain.go, it hangs and fails at its 10-second guard. I reproduced that failure locally, then restored the fix.
  • TestFileChainRecorderFailsOnPlantedLockDirectory checks the same case end to end through WriteEvent, which now fails after the 3-second lock timeout.
  • TestFileChainRecorderWaitsForStaleLock, the stale regular-file case, still reclaims the lock.
  • go test ./... passes and go test -race ./internal/audit ./cmd/nomos passes.
  • go vet ./..., gofmt -l ., and python3 scripts/check_docs.py are clean.

Compatibility And Docs

  • Behavior: unchanged in normal operation.
  • Edge case: a lock path that is not a regular file now makes audit writes fail, so the hook blocks the call instead of hanging. Previously this hung until the harness timed out.
  • Docs: CHANGELOG gains a 0.19.2 section.
  • Release: the fix: title cuts v0.19.2.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HnquYGTpTSAvnvp7UJTVpv


Generated by Claude Code

acquireFileLock's stale-lock branch removed the lock path and continued
without checking the deadline. When the path was something os.Remove
cannot delete, such as a non-empty directory with an old timestamp, the
loop spun forever. The hook then never answered, and Claude Code lets a
PreToolUse call through when its command hook times out, so the call ran
with no decision and no audit record. Code running as the user could
plant such a directory at .nomos/claude-code-hook.jsonl.lock.

The deadline now bounds every path through the loop, only a stale
regular file is reclaimed (Lstat, so a symlink is never followed or
removed), and a lock that cannot be taken fails the write. The hook
already exits 2 on an audit write failure, which blocks the call.

Found while researching how to keep the hook authoritative against code
the agent runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnquYGTpTSAvnvp7UJTVpv
@safe-agentic-world
safe-agentic-world merged commit 1f3aa2d into main Sep 26, 2026
40 checks passed
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.

2 participants