Skip to content

ISAStaleWriteGuard.recordView() hashes the whole file after an offset/limit Read, so a whole-file Write from a 5-line view passes the guard #2187

Closed

Description

@Bowlerjim

Observed on 7.54.0.

What

The hook's own header states its purpose:

stop a whole-file Write from silently discarding ISA changes the writing session never saw.

The view it records makes no distinction between a full and a partial Read:

// hooks/ISAStaleWriteGuard.hook.ts:113
export function recordView(sessionId: string, filePath: string): void {
  ...
  views[filePath] = hashOf(readFileSync(filePath, "utf-8"));

and the PostToolUse entry point records a view for any Read, ignoring tool_input.offset / tool_input.limit:

// hooks/ISAStaleWriteGuard.hook.ts:192
if (tool === "Read" || tool === "Write" || tool === "Edit" || tool === "MultiEdit") {
  recordView(input?.session_id, input?.tool_input?.file_path || "");
}

So a session that read 5 lines of a 200-line ISA is credited with a hash of all 200, and a later whole-file Write passes the guard. That is exactly the case the hook exists to catch.

Repro

# 12-line ISA whose C2 was closed by another session
echo '{"session_id":"sess-A","tool_name":"Read","tool_input":{"file_path":"<path>/ISA.md","offset":1,"limit":5},"tool_response":{}}' \
  | bun hooks/ISAStaleWriteGuard.hook.ts

echo '{"session_id":"sess-A","tool_name":"Write","tool_input":{"file_path":"<path>/ISA.md","content":"clobber"}}' \
  | bun hooks/PreToolGuard.hook.ts; echo "exit=$?"

Observed: the view file records the hash of all 12 lines after a read of 5, and PreToolGuard exits 0 — the whole-file Write is allowed.

Expected

A partial read is not a view. The Write blocks with the guard's stale-write message, or the path falls back to "never observed here", which is the documented fail-open state and is strictly safer than a false full view.

Fix sketch

In the import.meta.main branch, skip recordView when tool_input.offset or tool_input.limit is present — or record a "partial" sentinel that check() treats as a mismatch.

Raised as an issue rather than a PR because the choice between "skip the record" and "record a sentinel that actively blocks" changes how noisy the guard gets on large ISAs, and that is the maintainer's call.

Activity

  1. waveman2020-sudo commented on Sep 23, 2026

    @waveman2020-sudo

    This is Shiva, Eugene's AI Assistant, reporting on Eugene's behalf.

    Confirmed. recordView() hashed the whole file regardless of Read offset/limit, so a partial Read passed the guard as if the whole file had been reviewed. Now skips recordView() on partial Reads, falling back to the documented fail-open behavior.

    Environment: ThinkPad, Ubuntu 24.04.4 LTS, Ryzen AI 9 HX PRO 370, Bun 1.3.11.

  2. danielmiessler commented on Sep 28, 2026

    @danielmiessler
    Owner

    Thanks @Bowlerjim. We took the "skip the record" option with one refinement. A partial Read never advances a full view the session already has. If a partial Read is the session's first look at the ISA, it records a view, so an edit by another session afterwards still blocks a whole-file Write. There are tests for both paths. It goes out in the next public release (8.3.0).

  3. danielmiessler commented on Oct 3, 2026

    @danielmiessler
    Owner

    Closing this out. I checked the fix described above in source and its tests: a partial Read never advances an existing full view. Next release carries it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions