Did your AI agent get the memo, or just the blame?
I thought I had committed a rule for my AI coding assistants: “don’t put
another assistant’s work into your commit.” It went into my decisions ledger
(DECISIONS.md), a file of rulings for me and the assistants. A commit 67
minutes later carried another assistant’s changes. These assistants were
separate conversations with a coding tool, allowed to run Git commands
without asking my approval each time, sometimes in the same set of files.
Before blaming them, I had one rather basic thing to establish: had I
actually given them the rule in CLAUDE.md and MEMORY.md, the files
automatically loaded as instructions when each conversation started?
Where I’d left it
Several conversations with assistants, or sessions, could work in the same checkout. Earlier that same day, before I committed the rule, a commit had carried five files from another session’s content-generation pipeline without mentioning it in the message. The files survived; the explanation of why they belonged there didn’t.
The rule required staging to name explicit paths. Commands such as
git add -A that gathered everything were out.
The two commits made by assistants on my behalf, shown in Git’s local time:
18:57 Rule committed: stage explicit paths.
20:04 Commit includes three foreign lines in Git hook files.That’s 67 minutes between the displayed times. The later commit’s staging
command wasn’t recorded, so I can’t say it used git add -A. The diff does
show those three foreign lines, comments identifying the generated hook
version. The session that wrote them had also seen someone working in that
checkout and written there anyway.
The following morning’s analysis reported checking the instruction file
CLAUDE.md and the memory index MEMORY.md, both loaded when a conversation
started. It found the staging rule only in the ledger and a script comment,
absent from the two files every new conversation loaded.
That was my mistake. I’d recorded the decision without delivering the instruction. A new conversation received nothing new. An assistant might have opened the ledger separately; the record doesn’t establish whether the 20:04 committer had. It certainly doesn’t establish disobedience.
What could refuse it?
Getting the rule into the loaded instructions still left the assistant responsible for obeying it. I wanted an attempted broad staging command to fail. The assistant software’s own deny list was the obvious place to try, but a test recorded in the ledger found it inactive under the permission-bypass mode I used.
A hook run before a tool call could refuse the call in that mode. This hook belongs to the assistant software; the stamped files belonged to Git. The hook’s guard script checks the command and returns a refusal to the assistant. The check went in the day after I committed the rule.
Its testing suite passed these checks five weeks later:
- Blocks git add -A.
- Blocks git add --all.
- Allows git add -Ainside a commit message.
The last check matters. I needed to be able to write a commit message about
git add -A without being prevented from committing the check against it.
The guard recognises the command in command position; mentioning its name
is allowed.
That test shows a refusal works. It can’t tell me how many mistakes it prevented. The rule logged nothing for those five weeks. Its first three logged refusals most likely came from a test that deliberately triggered the guard; the log can’t distinguish tests from slips. I have no measured reduction to report.
A model’s analysis of one month of decision ledgers, memory files and an audit across my projects found recurrence after six of seven fixes that existed only as documents. Across seven fixes with blocking checks, none failed on the commands they were written to stop. The failures it did find used commands no check named. It also warned that a fix less than three days old hadn’t yet been tested by time.
- A rule needs somewhere to arrive and something that can say no.
I could now identify both. But what, exactly, had I made impossible?
The commit you didn’t make
Sixteen days after I committed the rule, a session staged thirteen explicit paths. It followed the rule. Before it could commit, another session committed those changes under its own message.
Your choice of files doesn’t control the next writer’s commit. Git has
one index, its staging area, per working tree. A plain git commit takes
everything in it, whoever put it there.
You can see this with one shell playing both sessions in sequence. I ran these commands in a fresh directory under a temporary parent, outside my repository:
git init -q one-tree && cd one-tree
git commit -q --allow-empty -m 'start'
printf 'mine\n' > mine.txt
printf 'yours\n' > yours.txt
git add mine.txt # session A
git add yours.txt # session B
git commit -q -m 'B: add yours.txt' # session B
git show --stat --format='%s' HEADThe actual output names the work the message left out:
B: add yours.txt
mine.txt | 1 +
yours.txt | 1 +
2 files changed, 2 insertions(+)Neither session staged broadly. To restrict the commit itself, -o, short
for --only, selects named paths. With both files modified and staged:
printf 'mine 2\n' > mine.txt
printf 'yours 2\n' > yours.txt
git add mine.txt yours.txt
git commit -q -o yours.txt -m 'B: yours.txt only'
git show --stat --format='%s' HEAD
git status --shortThis time my run printed:
B: yours.txt only
yours.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
M mine.txtmine.txt stayed staged and uncommitted. That protects existing, tracked
files in this direction. Change the input to a brand-new file:
printf 'new\n' > new.txt
git commit -o new.txt -m 'B: new.txt'The command exited with status 1 and printed:
error: pathspec 'new.txt' did not match any file(s) known to gitThe new file needs adding first, reopening the shared staging window. My automated writers now use a helper that locks their commits and names the paths. Separate assistant sessions still rely on a convention, and an unidentified writer took another session’s staged work a day after the thirteen-path incident.
My check works, and there’s still more to fix. Now I know what to test next.