Skip to content

[security] - Refuse bot patches that touch CI, hooks, agent config, locks or protected files - #1734

Merged
cgfixit merged 2 commits into
mainfrom
claude/apply-fixes-path-denylist
Oct 20, 2026
Merged

cgfixit merged 2 commits into
mainfrom
claude/apply-fixes-path-denylist

Conversation

@cgfixit

@cgfixit cgfixit commented Oct 20, 2026

Copy link
Copy Markdown
Owner

Proposed changes

#1709 item 6A. codex-apply-fixes.yml publishes a one-commit patch that Codex wrote from a "parent bot comment". That comment was itself generated from untrusted PR text, so this is second-order prompt injection.

The publish job's embedded Python check refused only:

  • secret-like files (.env*, id_rsa*, *.pem, *.key);
  • Git metadata;
  • symlinks and submodules.

A patch could still rewrite CI, hooks, agent config or the hashed locks. The check now also refuses these paths:

Category Paths refused Why
CI and hooks .github/, .githooks/ run with privileges on later events; GITHUB_TOKEN already can't push workflows, but .github/ holds more than workflows
Agent config .codex/ (the trusted prompts), .claude/hooks/, .claude/settings.json, .claude/settings.local.json, .mcp.json executed or trusted by other agents
Supply chain locks/ hashed locks; only the macOS refresh scripts may write them
.githooks/security.conf protected files data/personality/soul.md, utils/sanitizer.py, gate_auth.py, .gitleaksignore, .trivyignore.yaml, .osv-scanner.toml, .claude/skills/dotenv-guard/baseline.txt an edit silently changes or silences a control

A refused patch fails the publish job with Refusing protected patch path: <path>, and nothing is pushed. Ordinary paths, including .claude/skills/** and constraints.txt, still pass.

constraints.txt passes because a pin change there fails check_lock_contract.py until someone re-locks on the Mac.

Invariant / Governance Impact: none on the six invariants. This is a workflow-only change. It strengthens the governance boundary that protects soul.md (I5) and the sanitizer from bot edits.

Types of changes

  • Security (auth, privacy, injection, egress, redaction, or secrets)
  • CI / infrastructure (workflows, hooks, templates, or checkers)

Scope: Infrastructure / CI only.

Benefits / why

A prompt-injected bot comment can no longer turn @codex apply fixes into a commit that:

  • edits CI, hooks or agent settings;
  • swaps a lock hash;
  • rewrites the soul or the sanitizer.

This is enforced by code that runs on the publish runner, not by the prompt.

Risks to monitor

  • A legitimate fix to a denied path now needs a manual push. That is the intended tradeoff from ci tightening #1709.
  • The deny list is a prefix/exact match on Git's normalized paths. A protected file added later must be added here too. Its comment points at .githooks/security.conf, which is the other copy of that list.
  • Signal: a failed publish job reading Refusing protected patch path: ….

Checklist

  • I have read docs/THREAT_MODEL.md, INVARIANTS.md and SECURITY.md (and any relevant Phase docs)
  • This change preserves all 6 security invariants and I6 module isolation
  • Full sandbox validation has been run (per CLAUDE.md §5, left to CI)
  • No new external network dependencies or mandatory online LLM assumptions were introduced
  • Commit messages follow the title prefix convention above

Further comments

Run directly:

  • tests/test_codex_apply_fixes_path_guard.py (new, 25 tests) extracts the workflow's own embedded check and runs it with python -I against synthetic changed-path and index-mode files:
    • ordinary paths pass;
    • all 13 protected paths are refused by name;
    • the existing secret-like, Git-metadata and symlink refusals still hold.
  • With the workflow change stashed, the protected-path tests fail, so the test catches a missing deny list.
  • actionlint: clean.
  • zizmor 1.28.0 --offline: no findings.
  • ruff: clean.

Not run: a live @codex apply fixes. Codex reported no credits on earlier runs.

Suggested merge order of open PRs

Trial merges verified (git merge --no-commit --no-ff + abort, separately and stacked) against main @ 14812bd on 2026-10-20.

This PR · Blocked (draft)
Impact: apply-fixes path deny list.
Action: Merge when CI is green. Clean.

zizmor medium-severity PR · Blocked (draft)
Impact: ci.yml zizmor gate and comment.
Action: Independent; either order. Clean, also stacked.

ELI5

When you reply @codex apply fixes, a bot writes a patch based on another bot's comment, and that comment was itself written by reading the PR. Someone could hide instructions in a PR so the patch quietly edits something dangerous: the CI files, the git hooks, the AI agents' settings, the locked package hashes, the soul file or the injection filter.

The step that publishes the patch already refused secret files. Now it also refuses those paths and fails loudly instead of pushing. The list lives in the publish step of .github/workflows/codex-apply-fixes.yml.

  • Enhances: a hard wall around the files that control everything else.
  • Makes obsolete: trusting the prompt to stay away from them.
  • Retrieves: the same protected-file list the local git hooks already enforce.
  • Pushed to extremes: a real fix to one of those files needs a human push.

I ran the new 25-test check against the workflow's own embedded script. Only a real @codex apply fixes exercises it end to end.

🤖 Generated with Claude Code

https://claude.ai/code/session_014FZ8JTF2v1eQd1L5CKQR8j

Last updated: 2026-10-20 12:40 ET


Generated by Claude Code

…ocks or protected files

codex-apply-fixes.yml publishes a patch a bot wrote from PR text, and its
path check refused only secret-like files, Git metadata, symlinks and
submodules. It now also refuses .github/, .githooks/, .codex/,
.claude/hooks/, .claude/settings*.json, .mcp.json, locks/, and the
.githooks/security.conf protected files (soul.md, utils/sanitizer.py,
gate_auth.py, the scanner ignore files, the dotenv-guard baseline). Those
change by hand in a normal PR.

tests/test_codex_apply_fixes_path_guard.py runs the workflow's own embedded
check against synthetic path lists. Refs #1709 item 6A.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FZ8JTF2v1eQd1L5CKQR8j
@chatgpt-codex-connector

Copy link
Copy Markdown

Summary

  • Expanded the protected-path regression matrix to cover all exact deny-list entries, adding .claude/settings.local.json, .trivyignore.yaml, and .osv-scanner.toml. tests/test_codex_apply_fixes_path_guard.pyL62-L86
  • Committed the follow-up as 02feb61 ([security] - Cover every exact bot patch denial).
  • Created the follow-up PR metadata titled “[security] - Complete protected-path regression coverage.”

Testing

  • ✅ bash scripts/ensure-githooks.sh
  • ✅ python -m pytest -q tests/test_codex_apply_fixes_path_guard.py — 28 tests passed.
  • ✅ python -m ruff check --select F,B,S tests/test_codex_apply_fixes_path_guard.py
  • ✅ git diff --check
  • ⚠️ actionlint .github/workflows/codex-apply-fixes.yml — actionlint is not installed in the environment.
  • ⚠️ zizmor --offline .github/workflows/codex-apply-fixes.yml — zizmor is not installed in the environment.

View task →

.claude/settings.local.json, .trivyignore.yaml and .osv-scanner.toml were in
DENY_EXACT but had no regression case. Every exact and prefix entry is now
covered (28 tests). Raised by the Codex review on #1734; its own commit
was never pushed, so this adds the cases here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FZ8JTF2v1eQd1L5CKQR8j

cgfixit commented Oct 20, 2026

Copy link
Copy Markdown
Owner Author

The coverage gap the Codex summary points out was real: .claude/settings.local.json, .trivyignore.yaml and .osv-scanner.toml were in DENY_EXACT but had no test case. Its 02feb61 was never pushed, though. It doesn't exist on GitHub and the branch head didn't move, so I added the three cases here in 029a424. A scripted check now confirms that every exact and prefix entry in the deny list has a test (28 pass).


Generated by Claude Code

@cgfixit
cgfixit marked this pull request as ready for review October 20, 2026 19:04
@cgfixit
cgfixit merged commit 34b44e1 into main Oct 20, 2026
63 checks passed
@cgfixit
cgfixit deleted the claude/apply-fixes-path-denylist branch October 20, 2026 19:04
@cgfixit cgfixit mentioned this pull request Oct 23, 2026
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.

2 participants