Repository navigation
[security] - Split @claude into a read-only default and an owner-approved write mode - #1725
Merged
Merged
Conversation
…oved write mode claude.yml now runs a gate job that refuses fork PRs and picks a mode. Read is the default: contents read, and the edit/commit/web tools are denied. Write runs only when the owner's comment ends with the line "write access approved for this PR": contents write, web tools still denied. The mode is enforced by which job runs and its permissions, not by the prompt. Adds tests/test_claude_workflow_security_contract.py, which runs the gate's embedded script under Node against mocked PRs and comments. Refs #1709. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FZ8JTF2v1eQd1L5CKQR8j
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposed changes
#1709 item 1.
.github/workflows/claude.ymlused to run a single job withcontents: readandpull-requests/issues: write, with noclaude_args. Claude got the action's default tool set. Pushes could not succeed, but nothing said so, and there was no deliberate way to grant write.It is now three jobs:
gateif:as before: comment author, actor and triggering_actor are all the owner; open PR;@claudein the bodypull-requests: readreviewgatesaysread(the default)contents: read, PR/issues writeEdit,MultiEdit,Write,NotebookEdit,Bash(git add/commit/rm:*),WebFetch,WebSearchwritewrite access approved for this PR(case-insensitive, one trailing./!allowed)contents: write, PR/issues writeWebFetch,WebSearchThe fork-PR refusal (#1526 F8) moved from the old single job's first step into
gate. A fork PR fails there, so neither Claude job starts, even when the comment carries the approval line.Each mode also gets an
--append-system-promptframing line. Read mode is told to review only. Write mode is told to push only to the PR head, never force-push or rebase, follow CLAUDE.md, and leavelocks/,soul.mdand.githooks/alone. The framing is a soft control. The hard controls are job selection, job permissions and the deny list.Checked against the source of claude-code-action v1.0.252 (
f27be6b), not guessed:prompt:input switches a comment run from tag mode to agent mode (src/modes/detector.ts). The framing therefore goes throughclaude_args --append-system-prompt, which is the documented replacement forcustom_instructions.src/modes/tag/index.ts:Bash(git add:*),Bash(git commit:*),Bash(git rm:*)and the push wrapper. Edits are allowed through--permission-mode acceptEdits, which is whyEditandWriteare denied explicitly. Deny rules outrank allow rules.scripts/git-push.sh, refuses flags and any remote other thanorigin. It does not restrict the branch name. Staying offmaintherefore rests on the action's system prompt plusmain's branch protection. The public branches API reportsmainasprotected: true; I have not confirmed that the ruleset blocks aGITHUB_TOKENpush.Invariant / Governance Impact: none. This is a workflow-only change. No core file, graph edge, gate route or soul path is touched.
Types of changes
Scope: Infrastructure / CI only.
Benefits / why
.git/configremote URL.gateonly.Risks to monitor
.github/workflows/, becauseGITHUB_TOKENcannot, andmainprotection should stop a push tomain. Signal: an unexpectedclaude[bot]commit on the PR.cancel-in-progress: trueis unchanged. A second@claudecomment on the same PR cancels a running write job, possibly mid-push.issue_commentworkflows always run the default branch's copy, so a comment on this PR still runs the old single job. The first real test happens on the first@claudecomment after merge.Checklist
docs/THREAT_MODEL.md,INVARIANTS.mdandSECURITY.md(and any relevant Phase docs)Further comments
Run directly:
gatescript, under Node, via the newtests/test_claude_workflow_security_contract.py. All 13 tests pass on Python 3.12. They cover:needs/ifwiringactionlint: clean.zizmor 1.28.0 --offline(the CI pin): no findings, 5 suppressed.ruff: clean.Only CI ran: the full suites.
Not run: a live
@claudecomment. That is only possible after merge, as noted in Risks above.Post-merge smoke test:
@claude explain what this PR changes. Expect thegateandreviewjobs to run andwriteto be skipped.@claude fix the typo in X, withwrite access approved for this PRas the last line. Expectgateandwriteto run, and a commit on that PR's branch.Suggested merge order of open PRs
Trial merge verified (
git merge --no-commit --no-ff+ abort) against main @ 770d09e on 2026-10-20.This PR (#1725) · Blocked (draft)
Impact:
@clauderead/write split.Action: Review, mark ready, merge, then run the two-comment smoke test above. Clean.
#1724 · Blocked (red by construction)
Impact: dependabot's 55-package bump; the lock records were not regenerated.
Action: Do not merge. Close or curate on the Mac.
ELI5
Before,
@claudeon a PR was a reviewer whose pen had no ink. It could not save changes, but nothing said so, and there was no way to hand it ink on purpose. Now a doorman reads your comment first: thegatejob in.github/workflows/claude.yml.Normally the doorman sends in the reviewer without ink. That is the
reviewjob: itscontents: readtoken cannot push, and its edit and commit tools are switched off as well. If your comment's last line is exactly "write access approved for this PR", it sends in thewritejob instead, whose token can push to that PR's branch. Fork PRs are turned away at the door either way.The safety comes from which job runs and what its token may do, not from asking the model nicely. The one soft spot is the action's push helper, which does not check the branch name, so
main's protection is the real backstop. I ran the doorman logic, lint and zizmor directly. A live comment test is only possible after merge, because comment-triggered workflows run frommain.🤖 Generated with Claude Code
https://claude.ai/code/session_014FZ8JTF2v1eQd1L5CKQR8j
Last updated: 2026-10-20 09:35 ET