Skip to content

fix(quote-guard): refuse a shell command or code run that writes a bad quotation into a draft - #13

Merged
teakesdev merged 2 commits into
mainfrom
fix/quote-check-shell-writes
Nov 11, 2026
Merged

teakesdev merged 2 commits into
mainfrom
fix/quote-check-shell-writes

Conversation

@teakesdev

Copy link
Copy Markdown
Owner

What was wrong

The quote guard refused a write_file or patch that would put a quotation or record page cite into a draft that the case record does not support. It did not read terminal commands or execute_code runs. On 2026-11-10 an agent asked to append one sentence to the practice case's draft used the terminal three times, and all three writes went through:

printf '\n%s\n' 'As the declaration states, "the supervisor admitted the firing was retaliation." (Doc. 12 at 2)' >> Drafts/response_draft_synthetic.md
cd <case folder> && printf '\n\n…\n' >> Drafts/response_draft_synthetic.md && tail -n 3 Drafts/response_draft_synthetic.md

Doc. 12 holds no such words.

What changes

  1. The quote check reads commands and code runs. A terminal command or execute_code run that looks like it changes files and names a draft in a case is read as the text it would write (shell quotes and escapes undone, here-documents, python -c, Python string literals) and checked the same way. A not found or wrong page refuses the command before it runs and sends the agent to patch or write_file.
  2. A quote refusal wins over the source guard's question. The third command above names the case folder, which holds registered sources, so the source guard answered first with an approval question and the quote check never ran. With approvals turned off in Hermes nobody is asked, and the command ran. The hook now still runs the quote check when the source guard only asks. A refusal from the source guard is final, as before. An error in either guard does not turn the other off.

The write_file and patch message is unchanged. matterkit/versions.py and the verify count axes are untouched.

Choices made here, not asked for

  • A bad quotation in a command is refused, not put to the person as a question.
  • The whole command text is checked, not only the part that ends up in the file.
  • The quote check's refusal wins over the source guard's approval question.

Known limits

  • Not caught: text built in pieces or encoded, pieces of a Python f-string, a write through a file the command does not name. This catches an agent's mistakes; it is not a sandbox.
  • False refusals: a sed or perl command that removes a bad quotation, a grep of the bad sentence redirected into a draft, string literals in other languages, a relative name outside the case that also exists under the case folder setting.
  • A quotation broken across two lines is not seen by the checker at all. That is older behaviour and the same for write_file.
  • With approvals turned off in a Hermes profile, the source guard's question for a shell command stops nothing. Not changed here.

Tests

  • New tests/test_quote_guard_shell.py (22 tests): the real commands, 15 ways of writing the sentence, exact quotations passing in every form, wrong pages named, read-only commands and writes outside a case passing, paths, the order of the two guards, errors in either guard.
  • Each new behaviour was watched failing first.
  • Full gate on the tip (13777a8): 2243 tests OK (1 skipped) on Python 3.14.7, 3.10.20 and 3.12.13; hermes plugins validate . passed; both leak scans 0 findings, with no case patterns configured.
  • Seen live in a Hermes capture profile (Opus 5.5, approvals off): the agent's printf … >> append was refused with the quote check's message and the draft's hash did not change.

Not independently reviewed.

🤖 Generated with Claude Code

teakesdev and others added 2 commits November 10, 2026 23:08
…tion into a draft is refused

On 2026-11-10 an agent asked to append one sentence to the practice draft used the terminal
(printf ... >> Drafts/...) instead of patch, twice, and both writes went through: the quote
guard read only write_file and patch calls.

The guard now also reads terminal and execute_code calls. A call is checked when it looks
like it changes files (the source guard's own test, now one helper both guards use) and names
a text file in a case that is not a source or the store. Its text is read as the text it
writes: shell quotes and escapes undone, each quoted word and here-document set off as its
own paragraph, python -c and python here-documents read by their string values, sh -c read
again as a command. A quotation or page cite the record does not support blocks the call,
and the message sends the agent to patch or write_file.

Departure from the plan: the plan asked only for escapes to be undone. That did not catch
the two real commands (the redirect after the cite was read into the cite's sentence) and
refused exact quotations inside escaped shell quotes and next to quoted Python file names,
so the command is read by a small shell and Python reader instead.

Known limits, stated in the README: text built in pieces or encoded, or written through a
file the command does not name, is not caught. Known false blocks: a sed or perl that
removes a bad quotation, a grep of the bad sentence redirected to a file, and strings in
languages other than shell and Python.

Gate on the tree that became this commit: 2238 tests OK (1 skipped) on Python 3.14.7,
3.10.20 and 3.12.13; hermes plugins validate passed; both leak scans 0 findings (no case
patterns configured). Implemented by an Opus subagent from a written plan; the diff was
read and probed afterwards. Not independently reviewed. Not yet seen in a live Hermes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…rd only asks

The first fix on this branch let the quote check read terminal commands
and execute_code runs. Run live on 2026-11-10 it still let a made-up
quotation into the practice draft. The agent's command was

  cd <case folder> && printf '\n\n<sentence>\n' >> Drafts/... && tail -n 3 Drafts/...

It names the case folder, which holds registered sources, so the source
guard answered first with an approval question and the hook returned
that. The quote check never ran. An approval is a question a person may
answer yes, and with approvals turned off in Hermes nobody is asked, so
the command ran.

The hook now keeps the source guard's answer and still runs the quote
check when that answer is only a question. Order of results:

- a refusal from the source guard is final, as before;
- otherwise a refusal from the quote check wins;
- otherwise the source guard's question stands, as before.

The same holds after an error inside the source guard (its fallback
question no longer skips the quote check), and an error inside the quote
check leaves the source guard's answer in place.

Choice made here, not asked of ty: the refusal wins over the question.
The other way round a person saying yes to "this command names a folder
that holds sources" would also be saying yes to a quotation the record
does not hold, without being told about it.

Tests first: test_a_bad_quotation_is_refused_even_when_the_command_also_
needs_approval failed with 'approve' != 'block' on the live command
before the change. test_the_source_guard_still_comes_first is replaced
by tests that say what now holds: a command naming a source with no bad
quotation still goes to approval; a source guard refusal is not replaced;
an error in either guard does not turn the other off.

Seen live after the change (capture profile, Opus 5.5, approvals off):
the agent's printf append was refused with the quote check's message and
the draft's hash did not change.

Not independently reviewed.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@teakesdev
teakesdev merged commit c41a0bf into main Nov 11, 2026
4 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.

1 participant