
Once a headless Claude run passes, the last rung is delivery: branch, commit, push, and open the PR with nobody at the keyboard. The trap that bites everyone is the PR whose own CI never runs, because GitHub won't trigger workflows for events made by the default token.
Act II continues. We turn a passing headless Claude Code run into an open pull request with no human at the keyboard, and we walk straight into the pitfall that breaks most first attempts.
News (June 17–21, 2026):
git reset --hard, git checkout -- ., git clean -fd, git stash drop), blocks git commit --amend on commits it didn't make this session, and guards terraform destroy / pulumi destroy / cdk destroy. New attribution.sessionUrl setting omits the claude.ai link from commits and PRs. New /config --help; in the toggle, Esc now SAVES./config key=value inline from the prompt (works in -p and Remote Control), sandbox.allowAppleEvents, CLAUDE_CLIENT_PRESENCE_FILE to mute mobile pings, bundled Bun 1.4, line-by-line streaming, auto-retry on dropped connections.Tutorial — Auto-PR:
claude -p then git + gh, or a GitHub Actions workflow governed by permissions:.git push -u origin <branch> before gh pr create — the push prompt is fatal with no TTY.contents: write AND pull-requests: write; missing the second is a silent 403.GITHUB_TOKEN do not trigger downstream workflows. Use a GitHub App token so the PR's own CI actually runs.gh pr list --head guard. Empty-diff guard with git diff --quiet.--allowedTools prefix footgun: Bash(git push *) with the trailing space, not Bash(git push*).pull_request_target footgun only bites on untrusted fork code; internal nightly auto-PR is on the safe side.Quick news. Three point releases shipped between June seventeenth and twenty-first, and the most relevant one to today's episode is version two point one point one eight three, from the nineteenth.
This is auto-mode safety hardening, and it's directly load-bearing for auto-PR work. Auto mode now blocks destructive git commands when you didn't ask to throw away local work. So git reset hard, git checkout dash dash dot, git clean dash f d, and git stash drop all get refused unless you actually asked for that. It also blocks git commit amend when the commit wasn't made by the agent in the current session, so Claude won't rewrite your human commits. And there's an infrastructure guard: terraform destroy, pulumi destroy, and cdk destroy are blocked unless you named that specific stack. The upshot is that auto mode is now much safer to leave running toward a green run and a PR, because it won't nuke local work on its way there.
Same release adds an attribution control. There's a new setting, attribution dot sessionUrl, that omits the claude dot a i session link from your commits and pull requests. If you open public PRs and you don't want a claude dot a i link sitting in the body, flip that off. There's also a new config dash dash help that lists every shorthand key, and a behavior change in the config toggle worth memorizing: Enter and Space both change the selected setting now, and Escape SAVES and closes instead of discarding. So Escape no longer throws away your change.
Back up one release to version two point one point one eight one, from the seventeenth. The headline is config key equals value from the prompt. You can set any setting inline without the menu round-trip. Type config thinking equals false and it flips, mid-session, without leaving the prompt. It works in interactive mode, in dash p headless print mode, and in Remote Control. There's a new sandbox setting, allowAppleEvents, that lets sandboxed commands send Apple Events on macOS. There's a new environment variable, CLAUDE underscore CLIENT underscore PRESENCE underscore FILE, that points at a marker file to suppress mobile push notifications while you're at the machine, which is great for long unattended runs where you don't want redundant pings. The bundled Bun runtime jumped to one point four. Streaming got better: long paragraphs render line by line instead of waiting for the first line break. And API connection drops mid-thinking now auto-retry instead of showing that "connection closed while thinking" message. Plus fixes to prompt caching, file writes on network drives, macOS auth, and subagents.
Closing light on version two point one point one eight five, from the twentieth. The stream-stall hint got reworded to "waiting for API response, will retry in," and it now triggers after twenty seconds of silence instead of ten. Fewer false alarms on slow responses during long agentic runs. That's it for news.
Welcome back to Act Two. We've spent these episodes wiring Claude into systems under supervision, and today we climb one specific rung. You've got a headless run that passes. Now we ship it as a pull request, automatically, with nobody at the keyboard.
Let me set the substrate, because today builds directly on episodes you've already heard. We did the headless episode: the dash p flag and the Agent SDK, running Claude non-interactively so it prints and exits with a status code you can branch on. We did the GitHub Action: the anthropics claude code action, installed with install dash github dash app, the at-claude trigger on issues and PRs. We did the review-and-fix loop. We did the blast-radius episode: branch protection, least-privilege I A M, O I D C, and the pull request target footgun. We did the worktrees episode. We did the cost episode. All of that is the floor we're standing on now.
So here's the new rung. The headless run already passed. Tests are green, the diff is non-empty. What's missing is delivery. Branch, commit, push, open the PR. That's it. That's the gap we close today, and it sounds trivial until you try it unattended and hit four or five pitfalls in a row. We'll hit every one of them on purpose.
There are two homes for this logic, and you should know both. The first home is a local bash script, maybe on a cron schedule or on your dev machine, that wraps a claude dash p call and then calls git and gh. The second home is inside a GitHub Actions workflow, where the github token and the permissions block govern exactly what's allowed to run. Same goal, different environment, different failure modes. We'll write both.
Now the most important conceptual split of the whole episode. Who does the git and PR work? There are two answers. Answer one: Claude does it itself, inside the headless turn. You give it Bash git and Bash gh pr create in its allowed tools, and the model branches, commits, pushes, and opens the PR as part of its run. Answer two: you keep Claude to just the code change, and let plain deterministic shell do the gate, the branch, and the PR afterward.
The second approach is more idempotent and more auditable, and here's why that matters. The gate is the decision: are tests green, is the diff non-empty, is there already a PR open. In the shell version, that gate is plain bash you can read with your own eyes. In the Claude-does-everything version, the gate is something the model decided to honor, or not, based on your instructions. Most production setups put the gate and the PR mechanics in shell, and scope Claude to the edit. I'll present both and tell you when each fits, but I'm going to lean on the shell-gated version as the default, because in production you want the decision to be code, not vibes.
Let me walk the headless flags, because these are the dials you'll actually turn. This is all from the headless docs and the CLI reference.
The dash p flag, or dash dash print, runs non-interactively, prints, and exits. Every CLI option works with dash p. Then allowed-tools, the allowedTools flag, is the set of tools that execute without prompting you. It uses permission rule syntax. The docs example is claude dash p, "run the test suite and fix any failures," with allowed-tools set to Bash, Read, Edit. There's a mirror flag, disallowed-tools, for deny rules. A bare name removes the tool entirely, so just "Edit" kills Edit, star kills everything, m c p underscore star kills MCP tools. But a scoped rule like Bash open-paren r m space star close-paren leaves the Bash tool alive and only denies matching commands.
Now an important distinction people miss. The tools flag, plural, restricts which built-in tools EXIST at all. Empty string means none, "default" means the defaults, or you name them like Bash, Edit, Read. That's different from allowed-tools, which is about auto-approval. The docs say it plainly: to restrict which tools are available, use the tools flag instead. So tools is existence, allowed-tools is auto-approval. Keep those straight.
Permission-mode is the big one for CI. The values are default, acceptEdits, plan, auto, dontAsk, and bypassPermissions. The one you want for a locked-down CI run is dontAsk. The docs describe it as denying anything not in your permissions allow rules or the read-only command set. That's the fail-closed posture: if you didn't explicitly allow it, it's denied. Then acceptEdits writes files without prompting and auto-approves the safe filesystem stuff like mkdir, touch, mv, and cp, but other shell and network commands still need allowed-tools or a permissions allow rule, and otherwise the run aborts when one is attempted. And bypassPermissions is the same thing as the dangerously skip permissions flag, which is exactly what it sounds like and not what you want in an unattended pipe.
The output-format flag matters for the gate. Text is the default. J SON gives you a structured object with the result, the session I D, and metadata, and it includes total cost in U S D and per-model cost. You pull the result with jq, jq dash r dot result. There's also stream dash j son. And there's a json-schema flag that, combined with output-format json, returns validated J SON in a structured output field. That's perfect for a machine-readable gate decision, something like should-open-pr true or false, that your shell can test directly instead of grepping prose.
Two budget flags, and use both in CI. Max-turns limits the number of agentic turns, print mode only, and it exits with an error when it hits the limit. Max-budget-usd caps the dollars before stopping, also print mode only. So max-turns thirty and max-budget five dollars together mean the run can't loop forever and can't quietly cost you fifty dollars overnight. Remember the cost episode: programmatic usage is metered, so these are your guardrails.
Then there's append-system-prompt, and append-system-prompt-file. This is where you add instructions, and it's the natural home for your PR policy. Things like: only open a PR if tests pass and the diff is non-empty, and never open a duplicate. The model flag takes an alias, sonnet, opus, haiku, fable, or a full ID.
Now the bare flag, because it has a subtle catch. Bare is minimal mode. It skips auto-discovery of hooks, skills, plugins, MCP, auto memory, and your CLAUDE dot m d, so scripted calls start faster. It still has Bash, file read, and file edit. It sets an environment variable, CLAUDE underscore CODE underscore SIMPLE. The docs recommend it for scripted and SDK calls and say it'll become the default for dash p in a future release. Here's the catch: bare skips OAuth and the keychain, so your auth has to come from the ANTHROPIC underscore API underscore KEY environment variable, or an apiKeyHelper via the settings flag. And because bare skips your CLAUDE dot m d, it won't pick up your project's commit conventions unless you re-supply them, which you do through append-system-prompt. Hold that thought, it comes back when we talk attribution.
A few more headless flags worth a mention. Add-dir adds directories. The init flag runs Setup hooks before the session, in print mode. No-session-persistence skips saving the session. The worktree flag, dash w, runs in an isolated worktree under dot claude slash worktrees, which is a callback to the worktrees episode. And from-pr, with a number or URL, resumes sessions linked to a PR, and the docs note that linking happens automatically when Claude creates the pull request.
Now the single most important syntax detail in this whole episode. The docs have an example of Claude creating a commit itself: claude dash p, "look at my staged changes and create an appropriate commit," with allowed-tools set to Bash git diff, Bash git log, Bash git status, and Bash git commit, each scoped. And inside each of those, there's a trailing SPACE before the star. Bash open-paren git diff space star close-paren. That space is load-bearing. Bash git diff space star allows commands that start with git diff followed by a space. But Bash git diff star with no space would ALSO match git diff dash index, and other git diff hyphen subcommands you never meant to allow. That's a real footgun. So when you extend this for auto-PR, you write each one with the trailing space: Bash git checkout space star, Bash git switch space star, Bash git add space star, Bash git commit space star, Bash git push space star, and Bash gh pr create space star. Every single one with the space.
There's also a slick review pattern in the docs that needs no Bash at all. You pipe a diff straight into Claude: gh pr diff, the PR number, piped into claude dash p, with an append-system-prompt saying "you are a security engineer, review for vulnerabilities," and output-format json. Because you piped the diff in, Claude doesn't need any Bash permission to read it. That's the minimize-the-tool-surface move. Less surface, less to go wrong.
One auth note. Claude setup-token generates a long-lived OAuth token for CI and scripts, and that needs a subscription. For API billing instead, you use the ANTHROPIC underscore API underscore KEY.
Okay. Claude made the change. Now let's talk gh pr create, because the GitHub side has its own sharp edges. This is all from the gh pr create manual.
The flags you'll use: title, body, body-file which takes a dash for stdin, base for the target branch, head for the source which defaults to the current branch and supports user colon branch for fork heads, draft, reviewer which takes comma-separated handles with no spaces, label, assignee where at-me self-assigns, milestone, project, and fill which pulls title and body from your commit info. There's fill-first and fill-verbose variants, a template flag, and explicit title and body always override fill. There are also editor and web flags, but those are interactive, so they're not for CI. And there's a dry-run flag that prints instead of creating, but the docs warn it may still push git changes, so verify that before you trust it.
Now the gh behavior that will hang your pipeline if you don't know it. When the current branch isn't fully pushed to a remote, gh pr create shows a prompt asking where to push the branch. That prompt is fatal in an unattended run, because there's no T T Y to answer it. So the rule is: you push the branch yourself first. Git push dash u origin, your branch name, BEFORE you call gh pr create. Do not treat gh pr create as a push-for-me command in a non-interactive run. It isn't one. Push first, then open.
And gh authentication: gh reads GH underscore TOKEN first, then GITHUB underscore TOKEN. In Actions you set GH underscore TOKEN on the step from secrets. No token means it fails. Simple, but people forget the env line on the step.
Now Actions permissions, which is where the silent failures live. To open a PR from a workflow with least privilege, you set contents write so you can push the branch and commit, and pull-requests write so you can open the PR. And here's the trap: any permission you don't list is set to none. So if you carefully set contents write but forget pull-requests write, the push works, the commit works, and then opening the PR fails with a four-oh-three. And if your script doesn't have set dash e, or doesn't check exit codes, the whole run can LOOK green while no PR ever opened. So capture and assert the exit status of gh pr create. Don't trust the green check.
Now the big one. The pitfall I want you to remember if you forget everything else today. The github token does not trigger downstream workflows. This is verified GitHub behavior: events triggered by the github token, with the exception of workflow dispatch and repository dispatch, will not create a new workflow run. It's anti-recursion by design, so a workflow can't endlessly trigger itself.
Walk through what that does to you. Your nightly pipeline goes green. Gh pr create, authed with the default github token, opens a clean draft PR. And the PR's own on-pull-request CI never runs. Or it lands in an approval-required state with that yellow banner, "these workflows are awaiting approval from a maintainer," which needs a human with write access to click. Either way: branch protection requires the test check, that check never started, the required check sits unfulfilled forever, and the PR can't auto-merge. You wired the perfect pipeline and it's stuck. The recognition signature is: the PR opens fine, but the checks tab is empty or says awaiting approval, and auto-merge can't proceed.
Three fixes. One, use a PAT, a personal access token, as a secret instead of the github token. Two, and this is the clean one, use a GitHub App token. The actions create-github-app-token action mints an installation token, which is a distinct identity, and events from a distinct identity DO trigger workflows. Three, just accept the manual approve step, if a human clicking once is acceptable to you. For true no-human auto-PR, you want the App token.
Let me contrast that with what the Claude Code Action itself does, because it's deliberately more conservative. From its capabilities and limitations doc, it has smart branch handling. Triggered on an issue, it always creates a new branch. On an open PR, it pushes directly to that existing PR branch, no new branch. On a closed PR, it creates a new branch. And its "prepare pull requests" behavior creates commits on a branch and links back to a PREFILLED PR creation page. So by default, for human-author flows, the Action does NOT open the PR itself. It commits to a branch and hands you a prefilled link that still needs a human click. The Action also cannot merge, rebase, or do git operations beyond pushing commits.
That gives us three PR-opening mechanisms to compare. First, gh pr create, which is the script's choice and the most ergonomic for titles, bodies, and labels. Second, the GitHub REST API, a POST to repos owner repo pulls, which needs no gh install and can go through the github MCP create pull request tool, or curl, or octokit. Third, the Action's built-in prepare-PR-plus-link, which is the lowest effort but stops short of fully opening. Auto-PR deliberately goes further than that third option. We actually run gh pr create so no click is needed. That's the whole point of today.
Now attribution conventions, and this connects back to the bare-mode catch from earlier. By default, Claude Code appends "generated with Claude Code" and a Co-Authored-By Claude git trailer to commits, with a no-reply email, plus a PR-body footer. GitHub recognizes that trailer and shows Claude as a co-author. The blank line before trailers, the double newline, is required by git's trailer format, so don't drop it. This originates in the system prompt and is controlled by settings. The settings key is attribution in settings dot json, team-level in dot claude slash settings dot json or personal in settings dot local dot json. It's an object with commit and pr, and you set either to an empty string to disable it. Attribution supersedes the older boolean includeCoAuthoredBy, and if both are set, attribution wins. Fair warning: issues have been filed that it's intermittently not honored, so verify the resulting commit, don't just assume the trailer landed. And recall the new attribution session-url setting from the news segment, which strips the claude dot a i link specifically.
Here's the bare-mode tie-in. A bare-mode CI run skips your CLAUDE dot m d, so your commit conventions won't auto-load. Which means you re-state the commit format in append-system-prompt, or you pass a settings flag with an attribution block. Otherwise your nightly commits won't carry the trailer you expect.
Now idempotency, because an unattended job that runs every night will absolutely spawn a mess if you let it. Two layers to guard. Layer one: the branch already exists on the remote, so a re-run either fast-forwards, no-ops, or gets a rejected push. Layer two: an open PR already exists for that head branch. To guard the second, before you create, you run gh pr list with head set to your branch and state open, asking for the number field, and you count the results. If the count is greater than zero, a PR is already open, so you skip the create and maybe just push your update. Otherwise you open the PR.
And the foundation of all of this is a deterministic branch name. If you use a second-precision timestamp in the branch name, every run is a brand-new branch, and you get a parade of feature-one, feature-two, feature-three, a new PR every single night. Instead, derive the name from something stable: the issue number, a content hash, or a date stamp. Claude slash auto dash fix dash the date. Claude slash issue dash forty-two. With a deterministic name, re-runs converge onto the same branch instead of multiplying. And know this: gh pr create does NOT auto-update an existing PR, so you guard manually with that gh pr list check.
One more guard, the empty-diff guard. Before you branch, commit, and push, check whether there are actually changes. Git diff quiet exits zero when there's nothing, or you read git status porcelain. If the diff is empty and you branch, commit, push, and open a PR anyway, gh pr create errors with "no commits between base and head," or worse, opens a useless empty PR. The named pitfall is "pushing an empty diff." Don't do it. Check first.
Let me put the local script together in spoken form, because hearing the order is the point. Set bash strict mode, the dash e u o pipefail line. Define your repo directory, your base branch main, and a deterministic branch name like claude slash auto dash the date. Change into the repo, fetch origin, checkout main, and pull fast-forward only. Step one, Claude makes the change ONLY, no git or gh tools. Claude dash p with the task, in bare mode, permission-mode acceptEdits, allowed-tools scoped to Read, Edit, Bash npm test, and Bash npx tsc, with max-turns thirty, max-budget five dollars, output-format json, and you tee the J SON to a file so you have the record.
Step two, the deterministic gate, in shell. Run npm test, and if it fails, print "tests red, not opening a PR" and exit one. Then check the diff: if git diff quiet AND git diff cached quiet both say empty, print "empty diff" and exit zero. Notice the gate is shell, not Claude. That's the auditable part.
Step three, idempotency and delivery. If the branch ref already exists locally, check it out and try a fast-forward merge from main, otherwise create it with checkout dash b. Git add dash A. Commit with a message that includes the Co-Authored-By Claude trailer, with that required blank line before it. Then git push dash u origin the branch, because we MUST push ourselves before gh prompts. Then the idempotency check: gh pr list head the branch state open, count it, and if it's greater than zero, print "PR already open, pushed update only" and exit. Otherwise, gh pr create, base main, head the branch, a title, a body that says this is an automated change from a headless run with tests green and a non-empty diff and the generated-with-Claude line, and then draft, label automated and needs-review, reviewer your handle, assignee at-me.
A few things to narrate about that script. The draft flag is your safety valve. It opens the PR but signals not-ready and it won't auto-merge. The labels, reviewer, and assignee are for triage so a human knows what they're looking at. The gate is shell, not Claude. And git push dash u comes before gh pr create because of that no-T-T-Y prompt.
Now the variant where Claude does the whole thing in one turn, so you can compare. You write a single dash p prompt that says: if npm test passes and git status porcelain is non-empty, create a branch named claude slash auto dash the date, commit with a Co-Authored-By Claude trailer, push with dash u, and open a DRAFT PR to main with gh pr create. And first check gh pr list head the branch state open, and do NOT open a second PR if one exists. If tests fail or the diff is empty, do nothing. You give it allowed-tools for Read, Edit, Bash npm test, Bash git, Bash gh pr create, and Bash gh pr list, with permission-mode dontAsk and an append-system-prompt-file holding your PR conventions.
The trade-off is real. More autonomy means the gate depends on the model following your instructions, it's harder to audit, and you can't read the decision as plain code. So prefer the shell-gate version for production. And dontAsk here keeps it locked down, denying anything you didn't explicitly allow, which is the right posture when you're handing the model the git keys.
Now the Actions workflow, the second home. Picture a workflow named nightly-auto-pr. It triggers on a schedule, cron at six A M U T C daily, and also on workflow dispatch for manual runs. And here's a nice detail: workflow dispatch is one of the two events that DOES re-trigger downstream workflows, so a manual kick behaves differently from the cron run. At the top, the permissions block: contents write and pull-requests write, both, for the reasons we covered.
The job runs on ubuntu latest. First step, checkout, with fetch-depth zero so you have full history. Second step, and this is the fix, mint a GitHub App token with the create-github-app-token action, reading an app ID and a private key from secrets. Then set up node, npm ci. Then the headless Claude step: it runs npx claude code dash p with the task, in bare mode, permission-mode acceptEdits, allowed-tools Read, Edit, Bash npm test, with max-turns and max-budget set, and the ANTHROPIC underscore API underscore KEY in the step's env.
Then the gate-branch-commit-push-open-PR step, and the env on THIS step is the important part: GH underscore TOKEN set to the App token's output, NOT the default github token. Inside, strict mode, run npm test, the empty-diff guard, the deterministic branch name, git config a bot name and a bot no-reply email, checkout dash b or checkout the existing branch, git add dash A, commit with the trailer, push dash u, the gh pr list guard, and finally gh pr create with draft and a label.
The things to call out on the workflow. The permissions block is mandatory and minimal, and omitting pull-requests write gives you that silent four-oh-three. The GH underscore TOKEN is the App token, not the default github token, and that's the actual fix for the PR's CI never running. Also know this: scheduled cron runs always use the workflow file on the DEFAULT branch, and they run with the github token identity, which is another reason to swap in the App token. And branch protection from the blast-radius episode still applies. A bot can open a PR, but it cannot bypass required reviews or required checks, which is exactly what you want. A human, or an at-claude review, approves.
Let me land the real pitfall one more time, because it's the one that wastes a whole evening. You build everything right. Nightly goes green. Gh pr create opens a clean draft PR. You come back in the morning and it's stuck. Branch protection requires the test check, but that check never started, because GitHub deliberately does not trigger on-pull-request workflows for events created by the default github token. Anti-recursion. You see no checks, or that yellow "awaiting approval from a maintainer" banner needing a human click, which defeats the entire no-human-at-the-keyboard goal. Recognition: PR opens fine, checks tab empty or awaiting approval, auto-merge can't proceed. Fix: authenticate the create step with a GitHub App installation token, or a PAT, not the github token. The distinct identity makes the pull-request event trigger CI normally.
And the secondary pitfalls, fast, so you recognize each one. Empty diff gives you "no commits between main and the branch," so guard with git diff quiet. Duplicate PRs and runaway branches come from no gh pr list head check plus non-deterministic names, a new PR every run, so converge on a deterministic branch name and the guard. Missing pull-requests write is a four-oh-three, and if your script doesn't check exit codes the run looks green with no PR. The gh pr create push prompt in non-interactive mode hangs or fails, so always git push dash u first. The allowed-tools prefix bug, Bash git push with no space over-matches, so write Bash git push space star.
And the pull request target footgun from the blast-radius episode. If an auto-PR or at-claude review workflow uses pull request target AND checks out untrusted fork code, the fork's code runs with your secrets and a write-capable github token. That's exfiltration, remote code execution, push access. There are real-world exploits of exactly this. The rule: don't use pull request target unless you truly need it, and never check out untrusted PR code under it. Here's the reassuring part, though. Auto-PR runs that open PRs from your OWN trusted branches avoid this entirely. The danger is fork PRs. An internal nightly auto-PR, working on your own code, is on the safe side of that line.
So that's the rung. Claude makes the edit. A deterministic shell gate decides whether anything ships: tests green, non-empty diff, no existing PR. You push the branch yourself, then open the PR with gh pr create, draft and labeled. In Actions you set both permissions and you authenticate the create step with a GitHub App token so the PR's own CI actually runs. Most of the time, put the gate and PR mechanics in shell and scope Claude to the code change, because the decision should be something you can read, not something the model decided. That's auto-PR. Next time, we keep climbing toward the pipeline running itself end to end. See you then.