Skip to content

security(work-driver): a /work cycle can grant itself merge authority by editing AGENTS.md #406

Description

@randomm

The hole

resolveMergeAuthority reads the working-tree AGENTS.md:

// work-driver-merge-authority.ts:110
text = await fs.readFile(path.join(repoRoot, "AGENTS.md"), "utf8");

and it is called at the merged step (work-driver-merged.ts:294) — which runs after commit-pr has integrated the developer subagents' patches into repoRoot.

grep -rniE 'protected.?file' extension/src/ returns nothing. There is no gate on what a develop-step patch may touch.

So a developer subagent that adds a sentence like "LLMs are allowed to squash merge PRs" to AGENTS.md gets that change integrated at commit-pr, and the merge gate then reads the file the cycle just wrote and grants itself authority.

This does not require malice. A cycle working on an issue that legitimately touches AGENTS.md — of which this project has had many — can do it by accident.

Prior art says this is the known shape

  • Amp shipped a CVE for exactly this: the agent could edit its own allowlist via prompt injection (Embrace The Red, Aug 2025). Any policy file the agent can write is not a policy file.
  • gh-aw refuses by default any patch touching AGENTS.md, CLAUDE.md, GEMINI.md, CODEOWNERS, .github/, .claude/ or package manifests. Its stated rationale: "This protects against supply chain attacks where an AI agent could inadvertently (or through prompt injection) alter dependency definitions, CI/CD pipelines, or agent behavior files."
  • Claude Code will not read autoMode from .claude/settings.json at all, because "a checked-in repo or a build step could otherwise inject its own allow rules."

The principle is uniform: the repository is not trusted to loosen policy.

What to build

A protected-path set that a develop-step patch may not modify without halting the cycle. At minimum: AGENTS.md, CLAUDE.md, .github/, agents.json, .pi/, CODEOWNERS.

The natural home is the consolidation path in work-driver-integrate.ts, or the develop verification gate — both already inspect the staged patch set, so this is a check on data the driver already has rather than new plumbing.

On a match: halt with a distinct cap and name the offending paths. Do not silently drop the hunk — a partial integration is worse than a stop.

An operator who genuinely wants a cycle to edit doctrine can do it outside /work, which is the right amount of friction for changing the rules the harness runs by.

Acceptance criteria

  • CANARY a staged patch touching AGENTS.md halts the cycle before merged. Must fail against main today, where it integrates silently.
  • The halt names the specific paths, so the operator can see what was attempted.
  • End-to-end: a cycle whose developer adds a grant sentence to AGENTS.md does not reach a granted merge authority.
  • Patches touching only ordinary source are unaffected — asserted, so the gate is not just "block everything".
  • The protected set is stated in one place and documented, not scattered.
  • Quality gate per AGENTS.md §1; docs updated.

Note on ordering

This is a prerequisite for #406 (LLM-judged, citation-verified policy resolution). Making the grant easier to express in natural language while the cycle can still write that natural language would make the hole wider, not narrower.

This work must ship as its own separate PR, independent of any other open issue.

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