Skip to content

detection: agent instruction and memory files are an instruction channel that earns no trait #151

Description

@blitzcrieg1

What happens

UNTRUSTED_INPUT_COMMAND matches exactly two things:

r"\bgh\s+(issue|pr)\s+(view|list|diff|comment)|"
r"\bgit\s+(fetch|pull|clone)\b"

That is content arriving from a code forge, which is the threat model in the Agent Data Injection paper the rule was built against. Reading an agent's own instruction or memory file earns nothing:

cat ~/.claude/CLAUDE.md                    -> []
cat AGENTS.md                              -> []
cat .cursorrules                           -> []
cat ~/.claude/projects/x/memory/MEMORY.md  -> []

Empty. Not a weaker trait, no trait. So a read of a poisoned instruction file cannot start the untrusted-input-then-risky-action chain, because nothing marks it as untrusted content.

Why it matters

These files are an instruction channel by design. The agent reads them and treats them as directives, which is the entire point of them. If one is poisoned, it is a prompt injection with a long fuse: it survives the session it was planted in and fires on every later session.

OWASP AST10 names memory files as untrusted content directly, and this project is otherwise covered on AST05.

Same shape as #142. A channel that carries instructions, not classified as one, so the detector is blind to it while looking correct.

The naive fix is wrong

Do not flag every read of CLAUDE.md.

These files are overwhelmingly written by the operator and read constantly. Treating every read as untrusted input would produce a critical on the first thirty seconds of every session, which is exactly the false-positive class 0.7.0 spent a release removing. It would be #44 again with a different filename.

The shape that is actually interesting

Not the read. The write followed by a read.

  • The agent, or content it fetched, modifies an instruction file
  • A later session reads it and acts

That is a two-step sequence across sessions, which is a different correlation window from anything the engine does today: every current rule correlates inside one session. A poisoned memory file is specifically an attack that crosses sessions, so the rule that catches it has to as well.

Worth considering alongside host-subagent-swarm-burst, which is the only existing host-scoped rule and the only precedent for correlating beyond a single session.

Open questions

  • Which paths count. CLAUDE.md, AGENTS.md, .cursorrules, .github/copilot-instructions.md, per-agent memory stores. This list is a maintenance burden and gets stale as agents add conventions.
  • Whether a write by the operator is distinguishable from a write by the agent. If it is not, the rule cannot separate "I edited my own instructions" from "the agent rewrote them", and that distinction is the whole finding.
  • Whether this belongs as a trait, a rule, or both.

Blocked by the freeze

traits.py and rules.py are hashed into the ruleset fingerprint. The dogfood clock restarted 2026-08-30 and closes no earlier than 2026-09-26, so this cannot land before then. #55 is the other change queued behind that, and both should go in one pass so the fingerprint moves once.

Source

Noticed while checking whether an unrelated article on agent memory design touched anything here. It did not, but the topic pointed at this. Verified against classify_command rather than assumed.

Activity

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