Agents Read Files Through the Shell: Where File-Anchored Memory Goes Blind

Memory anchored to a file is usually triggered by the agent's file tools, because those calls carry a clean path. Agents do most of their file access with shell commands instead. Two independent counts, the harness's own release history, and a design that resolves shell commands against the anchors that already exist.

A file-anchored note is a caveat with a path attached: this config is generated, edit the template; this test fails on the build host because of the timezone. It is supposed to reach the agent at the moment the agent touches that file. The natural place to catch that moment is the agent client's file tools (Read, Edit, Write), because those calls hand over a structured file path, and it is where vectr and Claude Code's own rule loader both started. Coding agents read and change a large share of their files with shell commands instead: cat, head, sed -n, grep, sed -i, a redirect, an edit run on another host over ssh. A trigger wired to the file tools never looks at those calls.

The share is not small. Counting my own agent sessions by command prefix, shell calls that start with a file-reading command outnumber Read-tool calls by about three to one, 11,685 to 3,786. Shell writes are fewer than tool writes, 1,543 against 2,648, which still makes them more than a third of the writes counted. A count published on a Claude Code issue in October, from a very different setup, points the same way.

vectr, the memory tool I build, has this gap on its public main branch: a file-anchored note fires on a Read of a config file and stays silent on a cat of it. Claude Code's path-scoped rules had the same gap and have been narrowing it release by release; on current versions a plain single-file read loads them, while pipes, a cd and every shell write still load nothing.

A path trigger should fire on any tool call that touches the path, and it should not assume the file is local. One way to get there without guessing is to resolve each shell command against the set of paths that already carry notes, rather than hunting for strings that look like paths. That design is not in any vectr release, and it has blind spots of its own, which fail just as quietly.

Part 01
The Trigger Is Keyed to the Tool
01

What File-Anchored Memory Is, and Why Hooks Key It to File Tools

A note anchored to a file has one useful moment. At session start it is noise, because the agent does not yet know it will open the file. After the edit it is late. The trigger's whole job is to find the moment just before the agent reads or changes the file.

The usual way to find it is a hook. Claude Code's PreToolUse event runs a command before each tool call, hands it a JSON description of the call, and adds the text the command returns in its JSON output to the model's context. A matcher selects which calls the hook sees, and the hooks reference says what it filters on: the tool name. The description carries tool_input, the call's arguments. For Read, Edit and Write those include file_path. For the Bash tool they include command, the string the shell will run.

That asymmetry is why file-anchored memory gets wired to file tools. A file_path is already a path: no parsing, no chance of mistaking a sed expression for a filename. A command string has to be interpreted first, and interpretation can be wrong. Skipping it looks safe because the cost is invisible. A hook that does not fire writes nothing anywhere.

Analogy: the sensor on the front door

A house has a motion light wired to its front door. Open the door and the hall light comes on. Most of the household comes home through the garage, which opens into the same hall. The light works every time the front door opens, and the hall is dark most evenings. Nobody complains, because a light that never came on leaves no trace that it should have.

The fix is a sensor in the hall, which responds to someone being there whichever door they used. Where the analogy breaks is the hard part: a hall sensor detects a body directly, and a trigger on a shell call has to work out from a string which file is involved.

02

Where vectr's File-Anchored Notes Go Blind

vectr stores notes an agent writes while it works and delivers them back through hooks, or through an experimental local proxy for clients without hooks. Its file-anchored kind is called gotcha, and the instructions vectr installs for agents describe it as a caveat that "resurfaces exactly when that file is touched". On the public main branch at commit 726e798 that holds for three tools.

The installer registers one PreToolUse hook with the matcher Edit|Write|Read|Bash (main.py, lines 1164-1175). The comment above it says Read was added because "a file-reading tool has just as deterministic a tool_input.file_path as Edit/Write", and Bash for a different purpose: notes tied to a family of commands, such as a caveat about the test runner. The same hook, with its earlier Edit|Write matcher, is walked through in Claude Code Hooks: A Practical Deep-Dive on Deterministic Agent Behavior.

The hook's handler then splits on the tool name:

agent/hook_cli.py at 726e798, lines 436-451
tool_name = (event.get("tool_name") or "").strip()
tool_input = event.get("tool_input") or {}
if tool_name == "Bash":
    command = (tool_input.get("command") or "").strip()
    if not command:
        return
    payload = {"command": command, "hook_event": "PreToolUse"}
    if session_id:
        payload["session_id"] = session_id
    notes = _fetch_recall(port, payload)
    _emit_hook_context("PreToolUse", notes)
    return
file_path = (tool_input.get("file_path") or "").strip()
if not file_path:
    return
payload = {"file_path": file_path, "kind": "gotcha", "hook_event": "PreToolUse"}

A Bash call forwards its command string and matches command triggers, then returns. The file-anchored lookup, the request with kind set to gotcha, runs only when tool_input has a file_path (the full branch, lines 426-455). The comment above it is accurate: a Bash call "carries no tool_input.file_path at all, only tool_input.command". With no file path there is no anchored lookup, even when the command is cat deploy/config.yaml.

The proxy window and edit retirement

The proxy draws the same boundary. From each request the client sends to the model it builds a window, a bounded slice of the recent conversation, and takes anchors from the tool calls in it using fixed keys: _FILE_PATH_KEYS = ("file_path", "path", "notebook_path") (request_window.py, line 25). The module's docstring states the rule, extraction is "keyed on known tool-input fields only", "never a regex scan of free text for path- or identifier-shaped strings" (lines 7-8). A Bash call has none of those keys, so a cat contributes no anchor. The rule against scanning free text is right for prose, where a path-shaped string is usually a mention. In a shell command, a path-shaped token is usually a file about to be opened.

Retirement follows the same line. Since release 1.7.0, a note delivered because its anchor file was touched stays eligible while the file is only being read, and retires the first time a request's window "shows the file actually edited" (CHANGELOG, lines 805-810). In code, "edited" means one thing: the window records a path as edited only when the tool call's name is in _EDIT_TOOL_NAMES = ("Edit", "Write", "MultiEdit", "apply_patch") (lines 151-159), and the gate keeps every anchored note whose path no edit tool touched in that request from being charged (gate.py, lines 466-473). A sed -i on the anchored file is an actual edit. The window never shows it, so a note first delivered by a Read stays eligible until some edit tool touches the file, which may be never.

Tool call on deploy/config.yamlHook delivers the noteProxy window sees the anchorCounts as an edit
Read toolYesYesNo (a read)
Edit or Write toolYesYesYes
cat deploy/config.yamlNoNoNo (a read)
sed -n '1,80p' deploy/config.yamlNoNoNo (a read)
grep -n timeout deploy/config.yamlNoNoNo (a read)
sed -i 's/30/60/' deploy/config.yamlNoNoNo
echo 'retries: 3' >> deploy/config.yamlNoNoNo
ssh buildhost cat deploy/config.yamlNoNoNo (a read)

vectr main at 726e798. A note with an explicit command trigger can still fire on a Bash call, but it fires for the command family (every sed), not for this file.

Part 02
How Much File Access Goes Through the Shell
03

Counting Agent File Access by Tool: Shell Reads Against Read Calls

The first count is mine, from my own agent sessions, and crude by design. A Bash call whose command starts with a file-reading program (cat, head, tail, sed, awk, grep, rg, jq and similar) counted as a shell read, and a Read call as a tool read. Shell writes were Bash calls using sed -i, tee or an output redirect; tool writes were Edit and Write calls. Records were de-duplicated by unique record id, because a session transcript can hold the same record more than once.

Through the shellThrough file toolsShell share
Reads11,6853,78675.5%
Writes1,5432,64836.8%
All file access counted13,2286,43467.3%

My own sessions, counted by command prefix and de-duplicated by record id. Not every shell call counted here names a file.

The ceiling on a trigger that only watches file tools

Let T be the number of accesses to a file through file tools and S the number through the shell. A trigger that reads only file_path can see at most

coverage = T / (T + S)

For reads in this count that is 3,786 / 15,471 = 0.245. For writes it is 2,648 / 4,191 = 0.632. Across both, 6,434 / 19,662 = 0.327: about one access in three.

Two assumptions sit under those numbers. One is that every counted shell call names a file, which inflates S: a jq on piped input names none. The other is that anchored files see the same tool mix as files in general. Nobody has measured that, and I doubt it holds evenly; a config the agent edits often may draw more Edit calls, a log it tails almost none. Read the ratio as the size of the problem, not as the coverage of any one note.

The prefix count misses access in the other direction too: a Python script that opens a config, a build target that regenerates a file, a git checkout that rewrites a tree. I would not defend the third significant figure of any of this. The order of magnitude is the finding, and it says the file tools are the minority path.

04

An Independent Count of Shell File Access From a Different Setup

The second count is public. JhouCode, who maintains hipocampo, an MIT-licensed memory system built on hooks, posted two comments on anthropics/claude-code#78795, a feature request I opened in July for memory files that load on a trigger. The first, on 7 October, describes a PostToolUse hook with the matcher Edit|Write|Bash that checks each touched path against a table of patterns and delivers the warning for any row that matches.

The second comment, on 12 October, opens with the setup, and the setup shapes every number in it. The client runs from the filesystem root rather than a project folder, and most of the work lives on other hosts reached over ssh. Locally as well, the comment reports, "the agent here reads mostly through the shell": 1,253 Read calls in the last 30 days against at least 3,151 shell commands that open a file. The 3,151 comes from a prefix match, so it is a lower bound, and the ratio is at least 2.5 to 1.

The same comment reports a measurement, taken in August, of Claude Code's loader for nested CLAUDE.md files. From the root every project's CLAUDE.md is nested, so it loads only when something under it is read. Of 196 nested injections across 89 transcripts, 126 followed a Read and 55 a Bash call, and 40 of those 55 never named the path; the comment classes them as re-injections. A plain cd into the folder delivered nothing. The comment adds its own scope limit: a session started inside the repo would load that file at startup, so these figures describe behaviour from the root.

Two counts with different setups and different methods land on the same side: most file reads go through the shell. The second comment's advice for a native feature follows from that. The trigger "should not depend on Read, and should not assume the file is local: match Read, Edit/Write and the command text." Its hook also escalates. Once a warning has been ignored, a PreToolUse hook returns permissionDecision: "ask" before the next change, so the person running the agent has to approve it.

05

Claude Code's Path-Scoped Rules Have Been Closing the Same Gap

Claude Code ships its own file-anchored memory. A rule file in .claude/rules/ can carry paths frontmatter, a list of glob patterns, and the memory documentation covers the syntax. In August, as #88565 quotes it, the documentation said that "Path-scoped rules trigger when Claude reads files matching the pattern, not on every tool use". Whether a cat counts as reading a file has been settled in public, one release at a time.

Path-scoped rule loading, release by release

Date (UTC)SourceWhat changed or was reported
8 Jan 2026#16853 openedPath-scoped rules "are never automatically loaded into context when reading or editing files that match"
19 Aug 2026#16853 closedThe isolated failure was paths written as a YAML list, fixed in 2.1.84; a symlinked-path case was fixed in 2.1.198
21 Aug 2026#88565 openedRules load only on Read, Write or Edit; a file changed through Bash never loads them, and auto mode instructs Bash
2 Oct 20262.1.288Write and Edit now load rules in their scope "(previously only Read loaded them)"
7 Oct 20262.1.293Rules and nested CLAUDE.md load on "a single-file cat, head, tail, sed -n or grep command in the Bash tool"
13 Oct 2026Comment on #88565Measured on 2.1.296: what loads rules and what loads nothing

Issue dates from GitHub; release dates are each release's UTC publication date.

#16853 turned out to be about syntax: its closing comment, from a maintainer, traces the failure to a paths list that never matched. The tool question arrives with #88565, which quotes the auto mode system prompt "verbatim from the binary" of version 2.1.238:

Do your work through the Bash tool wherever it can accomplish the job: read files with cat, head, or sed -n, search with grep and find, and make file changes with sed, heredocs, or short scripts, rather than using the dedicated Read, Edit, or Write tools.

In that mode the instructed path for changing a file was the one path on which rules never loaded. The report's example: an infrastructure repo governed its central config with three path-scoped rules, the agent edited the config with a python3 one-liner through Bash, none of the three loaded, and the change shipped with its companion documents in the wrong format.

The tool mix is set by the prompt

In January the 2.1.21 release note says Claude was improved "to prefer file operation tools (Read, Edit, Write) over bash equivalents (cat, sed, awk)". By August, auto mode's system prompt said the opposite. Neither change had anything to do with memory, and each moved the share of file access a tool-keyed trigger can see. Whoever edits the next system prompt will not be thinking about your trigger either.

Where rule loading stops on current versions

Releases 2.1.288 and 2.1.293 then moved the harness's loader toward the file. The 13 October comment measured where it stopped, on 2.1.296, with fresh sessions and an InstructionsLoaded hook as ground truth. Rules loaded on Read, on Write, and on "a plain single-file cat/head/tail/nl/bat/sed -n, or grep/rg with a match", including inside a ; or && list. Nothing loaded on "pipes, ||, redirects (2>/dev/null), extra flags (cat -A), cd dir && cat file, grep -r, a grep with no match, and every Bash write".

So the harness has taken the conservative half: a command it can recognise outright as a read of one named file. Anything with a second moving part is left out, and so is every write. For rules I think that is a defensible place to stop. For file-anchored caveats it is the wrong place, because a caveat usually exists for the moment just before the file changes.

The same comment reports a workaround that keeps Bash: a root CLAUDE.md brief asking for one plain read of a file in each folder before working there. In auto mode, over 5 edit tasks run 3 times each, the rule loaded in 5 of 15 runs without the brief and 15 of 15 with it. The comment calls it a "Small sample, and it's a workaround, not a fix".

JhouCode's loader figures predate 2.1.293, so a current version would load on some of those shell reads. None of this reaches hooks. #89716 notes that the permission layer applies Read and Edit deny rules, in the permissions reference's words as the issue quotes them, "to file commands Claude Code recognizes in Bash, such as cat, head, tail, and sed", while a hook gets only the raw command: "The gap this report is about is that hooks cannot reach that recognition." A memory tool that delivers through hooks has to do that recognition itself.

Part 03
Resolving Shell Commands Against Known Anchors
06

Match the Shell Command Against Anchors That Already Exist

The fix keeps vectr's rule against guessing and applies it to the right object. The command is split into the words the shell would see, and each word is checked against the paths that already carry notes. A word that names no anchor contributes nothing, so the match never has to decide what an arbitrary string means. None of this is in a vectr release. It is a design, and the sketch below is what I ran to test it.

  1. Tokenize with Python's shlex in POSIX mode with punctuation characters on, so "my dir/notes.txt" stays one word and |, >, >> and && split off even when glued to a filename.
  2. Drop operators. For a word containing = (--config=x, CONFIG=x), also test the part after the first equals sign.
  3. Normalize each word as a path: join a relative word to the working directory and collapse . and ...
  4. Exact-match against stored anchor paths and path-trigger globs. A word that is itself a glob, such as deploy/*.yaml, is used the other way round: each anchor is tested against it.
  5. Deliver what a file-tool call on each matched path would deliver, in the same hook response.
Sketch: anchors a shell command names (Python 3, standard library only)
import os, shlex
from pathlib import PurePosixPath

OPERATORS = {"|", "||", "&", "&&", ";", "(", ")", "<", ">", ">>", "<<", ">&", "<&", "|&"}
GLOB_CHARS = set("*?[")

def shell_tokens(command):
    lex = shlex.shlex(command, posix=True, punctuation_chars=True)
    lex.whitespace_split = True
    try:
        return list(lex)
    except ValueError:              # unbalanced quote: fire nothing
        return []

def glob_match(path, pattern):
    return PurePosixPath(path).match(pattern)   # * stays inside one directory

def anchors_touched(command, cwd, anchors, globs=()):
    hits = []
    for tok in shell_tokens(command):
        if tok in OPERATORS:
            continue
        words = [tok]
        if "=" in tok:                       # --config=x, CONFIG=x
            words.append(tok.split("=", 1)[1])
        for word in words:
            if not word:
                continue
            path = os.path.normpath(os.path.join(cwd, os.path.expanduser(word)))
            if path in anchors or any(glob_match(path, g) for g in globs):
                found = [path]
            elif GLOB_CHARS & set(word):      # cat deploy/*.yaml
                found = sorted(a for a in anchors if glob_match(a, path))
            else:
                found = []                   # names no anchor: contributes nothing
            for p in found:
                if p not in hits:
                    hits.append(p)
    return hits

In the hook, the Bash branch would forward the command along with the event's cwd, which the hooks reference defines as the "Current working directory when the hook is invoked" and which vectr already records for each shell call it logs (hook_cli.py, line 343). The match belongs in the daemon, which holds the anchor set and already tokenizes commands with shlex for its command triggers (app/cmdnorm.py). The Bash branch already makes one request per call, so this adds a field, not a round trip. The proxy is harder: a request to the model carries no working directory per tool call. It can match absolute paths exactly and root relative ones at the workspace, as vectr's path triggers have done for relative inputs since 1.11.0 (CHANGELOG, lines 400-404), which is right while the shell sits at the root and wrong after a cd.

Why matching known anchors keeps false positives low

Let A be the set of anchored paths, and let a command split into tokens t1 ... tk. The literal part of the match is

fires(cmd) = { a ∈ A : norm(ti, cwd) = a for some i }

Here norm is step 3. The anchor set is small and known in advance, so the cost is k set lookups per command whatever its size, plus a test per path-trigger glob, and a test per anchor for a token that is itself a glob. A path regex has a harder question to answer for every token: is s/30/60/ a path, is 1,80p, is /dev/null? Every wrong answer becomes a lookup or a fire. Exact matching never asks. A false fire needs a token that normalizes to an anchored path in a command that does not open the file, like echo deploy/config.yaml or git log -- deploy/config.yaml. Both mention the very file the note is about, which is the cheapest way to be wrong.

Regex rows and changed-file detection, compared

Two other designs are public. hipocampo matches each row's regular expression against the file path or the raw command text (alvo_do_hook in gatilho.py), and its docstring is candid about the price: "The false positive (grep router.py) is accepted and bounded by the once-per-session marker." #88565 proposes that after a Bash call the client find which files changed on disk and run the same paths matcher over them; a reply on that issue points out that "a changed-files detector misses cat entirely".

DesignShell readsShell writesFires before the callFiles a script opensFalse positives come from
file_path only (vectr main)NoNoYesNoNothing
Pattern regex over the command text (hipocampo)YesYesWarning after, escalation beforeNoAny text that matches a row's pattern
Changed files after the call (#88565 proposal)NoYes, localNo, one call laterYes, if it writesAnything else that changes the tree during the call
Token match against known anchorsLiteral pathsLiteral pathsYesNoCommands that name an anchored path without opening it

The regex design has one real advantage over the token match: a row pattern can match a path inside a quoted remote command, which a token match sees as one word.

07

What a Token Match Catches in Shell Commands, and What It Misses

I ran the sketch above against the commands below, with the working directory /repo and two anchors: /repo/deploy/config.yaml and /repo/my dir/notes.txt.

CommandResultWhy
cat deploy/config.yamlFiresLiteral relative path
grep -n timeout deploy/config.yaml | headFiresThe pipe is its own token
cat deploy/../deploy/config.yaml 2>/dev/nullFires.. collapsed; 2, > and /dev/null are separate tokens
sed -i 's/30/60/' deploy/config.yamlFiresSame path as a read
echo 'retries: 3' >>deploy/config.yamlFiresA glued redirect still splits off its target
cat "my dir/notes.txt"FiresQuotes honoured
CONFIG=deploy/config.yaml ./run.shFiresThe part after = is tested too
cat deploy/*.yamlFiresAnchor tested against the glob
cat <<EOF > deploy/config.yaml (heredoc)FiresRedirect target; the body's lines are tokens too
ls -la && git statusNothingNo token names an anchor
cat $CONF, cat $(git ls-files)MissesThe path exists only at run time
cd deploy && cat config.yamlMissesResolved against /repo, not /repo/deploy
grep -rn timeout deploy/MissesA directory operand names no anchor (deliberate)
find . -name config.yaml | xargs catMissesThe file list is produced at run time
python build.pyMissesThe script opens the config itself
ssh buildhost cat deploy/config.yamlFiresOn the local anchor: right only if both hosts hold the same tree
ssh buildhost "sed -i 's/30/60/' /srv/app/deploy/config.yaml"MissesThe quoted remote command is one token
cat 'unbalancedNothingThe tokenizer raises; the match fires nothing

Output of the sketch above on Python 3.14. Highlighted rows are the limits.

Runtime paths, cd and remote hosts

A variable or command substitution produces its path when the command runs, after the hook has returned, and a find or xargs pipeline builds its file list then too. A script that opens files itself gives the hook nothing to read; only a filesystem-level signal could see python build.py open its config. The directory miss is deliberate. grep -rn timeout deploy/ does read the anchored file, but firing every note under a named directory would bury the one that matters.

An earlier cd in the same command is worse than a miss, because it can resolve to the wrong file: if /repo/config.yaml were also anchored, cd deploy && cat config.yaml would fire that note. Following a top-level cd through a && or ; list is tractable; following one through a subshell, pushd or a function is not. Claude Code's loader, in the 13 October measurement, loads nothing for cd dir && cat file. A heredoc is caught at its redirect target, but its body is tokenized too, so a heredoc that mentions an anchored path fires that note.

Remote paths are where "should not assume the file is local" bites. An unquoted ssh buildhost cat deploy/config.yaml resolves against the local working directory and fires the local anchor: right when both machines hold the same checkout, wrong when they do not. A quoted remote command is one token and misses. Handling remote files means anchors that can name a host, so buildhost:/srv/app/deploy/config.yaml is a different anchor from the local file, and re-tokenizing the remote command of an ssh call to match it only against that host's anchors. Absolute remote paths can be matched that way. Relative ones cannot, because the hook does not know the remote working directory.

Every miss is silent

A highlighted row raises nothing. The note does not arrive, and the transcript looks the same as a session where no note existed. The file-tool-only trigger fails in exactly that way, which is how its gap went uncounted. Any version of this fix needs a fire count per tool from the day it ships.

Interactive · Demo 1
Which Anchored Files Does This Command Touch?

Pick or type a command and compare what a file-tool-only trigger sees with what a token match finds. Put a cd before the cat, or use a variable or a quoted remote command, and the match goes quiet without any error.

Anchors: /repo/deploy/config.yaml · /repo/my dir/notes.txt · /repo/config.yaml
Tokens the shell would see
Normalized against the working directory

A small JavaScript port of the Python sketch: quotes, escapes and operators are handled; variables, globs and substitutions are not expanded, which is the point. Cyan tokens matched an anchor exactly; purple tokens are globs an anchor matched.

08

Should a Shell Write Retire a File-Anchored Note?

The token match says a command names an anchored file, not whether it changed it. sed -n and sed -i name the file in the same position, and cp deploy/config.yaml /tmp/backup.yaml names it as the source of a copy. For delivery the difference does not matter: a caveat is worth showing before a read as well as a write. For retirement it decides everything, because retirement is what stops the note being delivered again.

The two mistakes have different prices. Count a shell call as an edit when it was not one, and the note retires early: the agent backs up the config, the caveat is charged, and it is gone when the real edit comes. Miss a real shell write, and the note stays eligible and may be delivered again on the next touch. The first loses the note. The second costs some context and loses nothing.

hipocampo faced the same choice when deciding whether an ignored warning should escalate, and wrote the asymmetry into its code: "Bash is a READ by default, on purpose: erring low lets one ride pass; erring high blocks the owner's work by mistake. The two errors do not cost the same." (e_escrita in gatilho.py). Its pilot also caught a compound command misread as a write: "A write in one piece contaminated the read in another". It now checks writes per segment of a compound command (escrita_do_item).

For vectr that argues for counting only writes the tokens prove, a sed -i whose file operand is the anchor or a redirect whose target is the anchor, and treating everything else as a read. Whether even those should retire the note is open. A sed -i shows that the file changed, not that the change the caveat was about has been made. Edit-tool calls carry the same ambiguity today and retire the note anyway.

09

Testing Shell-Aware Anchors and Recounting Fires per Tool

The tests follow the rows above, and each asserts the notes delivered rather than the paths matched:

  • cat deploy/config.yaml from /repo and cat config.yaml from /repo/deploy deliver the same note, and so do the absolute form and ./deploy/../deploy/config.yaml.
  • sed -i 's/a/b/' deploy/config.yaml, >> deploy/config.yaml and the glued >deploy/config.yaml deliver it.
  • cat "my dir/notes.txt" delivers the note for the quoted path. The unquoted form delivers nothing, which is right: the shell would open my and dir/notes.txt.
  • ls -la && git status runs no anchored lookup, and the hook's output is byte-identical to the output without the change. An unbalanced quote delivers nothing and raises nothing.
  • cd deploy && cat config.yaml is pinned as a known miss, so a later change to that behaviour fails a test instead of passing unnoticed.

Agent Memory Fails in Three Places and Gets Measured as One argues that once delivery moves into the harness, the never-asked failure "moves into the harness, as a trigger that should have fired and did not". A shell access the file-tool trigger skips is that failure, and an outcome score cannot tell it apart from a note that was delivered and ignored. That is why the recount has to be per tool.

Recounting anchored-note fires per tool

Tests show the match does what it says. Whether it closes the gap is a measurement on the same kind of sessions as the counts of file access by tool, and before the change shell deliveries are zero by construction.

A recount that cannot grade itself

For tool u, let Fu be deliveries of anchored notes and Au accesses to anchored files. The quantity to compare is the delivery rate

ru = Fu / Au

for u = Read, Edit/Write and Bash. The trap is ABash. Counted with the token match itself, it agrees with FBash by construction and the rate comes out near 1 whatever the match misses. The denominator needs a looser, independent instrument, such as Bash calls whose command text contains an anchored file's basename. That overcounts mentions, so rBash below rRead mixes real misses with the instrument's own slack. A large gap is still a real one, and the transcripts behind it show which limit caused each miss.

Count the hook and the proxy separately. They resolve relative paths differently, so a gap between their shell delivery rates measures how often the agent works away from the workspace root.

Close
The Trigger Should Follow the File
10

Key File-Anchored Memory on the Path, Not on the Tool

vectr's instructions promise a note that "resurfaces exactly when that file is touched". The implementation keeps the promise for three tools, and in my count those tools carry about one file read in four and under two writes in three.

Claude Code's rule loader spent this year moving from the tool toward the file, and it now stops at reads it can recognise outright. I think that line is right for rules and wrong for caveats, because a caveat is usually about a change, and shell writes were more than a third of the writes counted here. Matching shell tokens against the anchors that already exist covers literal paths without guessing at anything else. What it leaves dark, the variables, the scripts, the cd, the remote edits, should be counted per tool rather than assumed small.

11

Sources