Read Gates - xsyetopz/claudetto GitHub Wiki

Read Gates

This page describes the reads slice of the core plugin claudetto. It is for a user who wants to know why a Read or an edit gets a deny, and for a contributor who edits the slice.

The Problem

Three failures repeat in long sessions:

  • The user rejects a call in the permission dialog, and Claude makes the same call again.
  • Claude reads a file again, and the file did not change, so the second read adds the same text to the context.
  • The user rejects an Edit or a Write, and Claude edits the file again with no new look at it.

The built-in dedup of Read covers only a read of the same range (see D60 in Engine Facts). The slice adds a tool.call hook with one gate for each failure. The hook makes no model call.

A Rejected Call Is Final

A tool.call hook wraps the permission check and its dialog. So the result of next shows a rejection of the user in the dialog. The test is an exact compare of the result with the text that Claude Code stores for a rejection (D61 in Engine Facts). The hook keeps a hash of the tool and the input of each rejected call. When the same tool comes with the same input in the same session, the hook denies the call. The reason tells Claude to ask the user, or to use another way.

A denial of the auto-mode classifier is not a rejection of the user, so the hook does not keep it.

No Read of an Unchanged Range Again

For each agent, the hook keeps the path, the mtime, the size, and the line ranges of each Read. A subagent has its own record, because it does not see the reads of its parent. A Read with no limit counts as the first 2000 lines. The hook denies a Read when all of these hold:

  1. The mtime and the size of the file are the same as at the earlier reads.
  2. The earlier reads cover the new range. Two reads that touch each other cover a range across both.
  3. The tool_result of each of those reads is in $.session.messages({ as: "api" }), and its content is not [Old tool result content cleared].

The deny reason gives the path and the earlier range. A Read with pages is not gated.

The hook denies a covered read one time. The same read again runs, because no hook sees the server edit clear_tool_uses that can remove the result. After a handoff or a compaction, the earlier result is not in the messages, so the read runs.

A Re-Read After a Rejected Write

When the user rejects a Write or an Edit of a file, the hook drops its record of reads of that file. It then denies the next Write or Edit of that file until Claude reads the file with Read. The reason tells Claude to read the file. An edit of another file runs.

Bounds

Bound Value File Use
READ_DEFAULT_LINES 2000 slices/reads/limits.ts Lines of a Read with no limit
MAX_SEEN_READS 64 slices/reads/limits.ts Reads that the record of one file keeps
MAX_REMEMBERED 500 slices/reads/limits.ts Files, rejected calls, and files to read again for each session

Cost

The hook makes no model call. A Read that an earlier read covers costs one $.session.messages call and saves the tokens of the second result.

Related Pages

⚠️ **GitHub.com Fallback** ⚠️