Codex Committed to the Wrong Branch (or Straight to main)

Codex landed your work on main, on a leftover branch, or split it across two — because cloud tasks default to your repo's default branch and local worktrees collide. How to force clean per-task isolation, plus how to rescue commits that already landed wrong.

You give Codex a small task (“rename this prop, update the call sites”). Twenty minutes later you check the PR list and there is none. You look at git log on main and the agent’s commits are sitting on top of main, no branch, no PR. Or it landed on feature/old-experiment from a teammate’s task three weeks ago. Or it created a branch but the final commit went somewhere else, so half the work is on agent/task-7 and half on agent/task-8.

Fastest fix: make the task start from a clean default branch and a fresh per-task branch every time, then protect main on the server so a stray push is refused. In cloud Codex that means setting the task’s base branch explicitly (cloud tasks default to your repo’s default branch as of June 2026); locally it means one Git worktree per task and a setup script that resets and branches before the agent touches anything. The rest of this article covers each cause, the exact commands, and how to rescue commits that already landed wrong.

First, figure out which Codex you are running

The two surfaces fail differently, so confirm which one you mean:

SurfaceWhere work happensDefault branch behaviorMost common wrong-branch cause
Cloud Codex (task from chatgpt.com/codex or the GitHub integration)A fresh, isolated microVM per task; destroyed on completionChecks out the repo’s default branch unless you pick a base branch or @codex fix on a PRBase branch defaulted to main; @codex fix ran against the default branch instead of the PR branch
Local Codex app / CLI (codex on your machine)Your checkout, or a Git worktree per taskInherits whatever branch the checkout is onUI shows one branch while the agent’s tool calls run in a different workdir; worktree name collision

If there is no PR but there are commits, it is almost always a base-branch or worktree problem, not a flaky PR step. Verify the branch first.

Common causes

1. Cloud task used the default base branch

A cloud task provisions a fresh container and “checks out your repo at the selected branch or commit SHA.” If you did not pick a base branch, it defaults to your repo’s default branch (main on most repos). The agent’s commits become a new branch only when you open the PR; if the PR step is skipped or fails, the work is effectively stranded on the base.

How to spot it: the task summary shows a diff but you never clicked “Create PR,” or the created PR targets main when you expected a release branch. In the task’s environment settings the base branch is unset or main.

2. Local UI shows one branch, the agent works in another

In the local Codex app the visible prompt can sit on one directory/branch while the agent runs tool calls with a different effective working directory. You think work is happening on agent/task-12; the agent is committing in a worktree (or another repo) entirely. This is a known issue (openai/codex #23140, duplicate of #21712) as of June 2026.

How to spot it: the branch shown in the UI header has no new commits, but git log --all --oneline finds your changes on a different worktree path or branch. Run git worktree list and check each path’s HEAD.

3. git checkout -b collided with an existing branch and the agent kept going

The setup script ran git checkout -b agent/fix-issue-123, but that branch already existed from a previous run, or the name is checked out in another worktree. Git refuses (git worktree enforces one branch per worktree). The command errored on stderr; the agent only inspected stdout and committed on the still-current branch.

How to spot it: the transcript shows fatal: A branch named 'agent/fix-issue-123' already exists. or fatal: 'agent/fix-issue-123' is already used by worktree at ..., followed by commits anyway. The commits went wherever HEAD pointed.

4. Agent synced with main, then forgot to switch back

Codex was told “stay in sync with main.” It ran git checkout main && git pull, edited files, and committed — without switching back to the task branch. The commits land on main locally. If the harness then pushes main, it hits branch protection (best case) or succeeds (worst case).

How to spot it: the transcript contains git checkout main with no matching git checkout agent/... afterward. The new commits’ parent SHAs sit on main’s tip.

5. AGENTS.md says “commit your changes” with no branch rule

A well-meaning instruction said “commit your changes” without naming a branch. Codex defers to the most explicit instruction it has; with no branching rule it uses HEAD. If HEAD is main, your work lands on main.

How to spot it: grep -in 'commit\|branch\|push' AGENTS.md finds no “create a new branch first” rule. The agent will not invent one.

6. Two parallel runs shared a worktree or volume

You triggered two tasks at once and both mounted the same persistent volume (self-hosted harness) or the same worktree path. They raced on git checkout -b; one won, both committed, and the loser’s commits mixed into the winner’s branch.

How to spot it: a single branch contains interleaved commits from two unrelated tasks. Worker logs show overlapping timestamps on the same volume.

7. Branch name templated from an unset variable

The setup script computes the branch as agent/${TASK_ID} and TASK_ID was empty. git checkout -b agent/ either errors or creates a literal branch named agent/, depending on Git version.

How to spot it: a branch literally named agent/, agent, or ${TASK_ID} appears in git branch -a.

Shortest path to fix

Step 1: For cloud Codex, set the base branch on the task

Before launching the task, set the base branch explicitly instead of trusting the default:

  • In the Codex web/task UI, the base branch is shown next to the repo selector. Change it from the repo default to the branch you actually want as the PR target.
  • For @codex on GitHub, comment on the PR itself (not the repo) so it works against the PR branch. If you comment on an issue, it branches from the default branch.
  • After the agent finishes, click Create PR and confirm the PR’s base branch is correct before merging. Cloud Codex names the head branch for you; you only choose the base.

This removes the single biggest cloud cause: an unset base silently meaning main.

Step 2: For local Codex, use one worktree per task

Git enforces one branch per worktree, which is exactly what you want for isolation. Create the worktree and start Codex inside it (there is no stable --worktree flag yet as of June 2026, so do this manually):

# Create an isolated worktree on a fresh branch, from up-to-date main
git fetch origin
git worktree add -b "agent/task-123" ../wt-task-123 origin/main
cd ../wt-task-123
codex

Each worktree has its own HEAD, so a second task cannot land on this branch by accident. Run git worktree list afterward to confirm the agent committed where you expect. For fully isolated per-worktree Codex config, set CODEX_HOME to a worktree-local path.

Step 3: In setup.sh, reset clean and branch before the agent runs

For any harness that starts on a reused checkout, force a clean state in .codex/setup.sh (or your task-init hook):

#!/usr/bin/env bash
set -euo pipefail

# Reset to a clean state, whatever the previous task left behind
git fetch origin
git checkout main
git reset --hard origin/main
git clean -fdx

# Create a fresh branch named after this task; abort if TASK_ID is empty
BRANCH="agent/${TASK_ID:?TASK_ID must be set}-$(date +%s)"
git checkout -b "$BRANCH" || git checkout -B "$BRANCH"
echo "Working on $BRANCH"

The :? guard refuses to proceed if TASK_ID is unset, killing the empty-template bug. The timestamp suffix prevents collisions on retry, and checkout -B resets the branch if a stale one survives.

Step 4: Pin the rule in AGENTS.md

Codex defers to the most explicit instruction it sees, so make this a top-of-file rule:

## Branching

- Always work on the branch created by setup.sh. Do not switch branches.
- Never run `git checkout main` mid-task.
- Never commit directly to main, master, develop, or release/*.
- To sync with main, run `git merge origin/main` from the task branch.

A specific rule near the top trumps the implicit “commit your changes” reading.

Step 5: Block direct pushes to protected branches on the server

This is the safety net for when everything above fails. In your GitHub repo settings, add a branch protection rule (or a ruleset) for main, master, develop, and release/*:

  • Require a pull request before merging (blocks direct pushes)
  • Block force pushes
  • Require status checks to pass

Even if Codex tries to push to main, the server refuses. See GitHub’s branch protection docs for the exact toggles.

Step 6: Add a pre-push hook that refuses protected refs

Catch the bad push at the sandbox layer, before the server sees it. In .codex/setup.sh after creating the branch:

mkdir -p .git/hooks
cat > .git/hooks/pre-push <<'EOF'
#!/usr/bin/env bash
while read local_ref local_sha remote_ref remote_sha; do
  case "$remote_ref" in
    refs/heads/main|refs/heads/master|refs/heads/develop|refs/heads/release/*)
      echo "ERROR: pushing to $remote_ref is forbidden for agent runs"
      exit 1
      ;;
  esac
done
EOF
chmod +x .git/hooks/pre-push

A server-side rejection sometimes leaves the sandbox in a confusing half-state, so failing locally first is cleaner.

Step 7: Verify HEAD in the commit step

Wrap the agent’s commit so it fails loudly if HEAD drifted off the task branch:

current=$(git rev-parse --abbrev-ref HEAD)
case "$current" in
  agent/*) ;;
  *) echo "Refusing to commit on $current"; exit 1 ;;
esac
git add -A
git commit -m "$1"

If anything switched HEAD off the task branch, the commit aborts instead of landing somewhere wrong.

Recovering when commits already landed wrong

If commits went to main locally but were not pushed:

# Save the bad commits onto a new branch (this just points a ref at HEAD)
git branch "agent/rescue-$(date +%s)"

# Reset main back to the remote
git fetch origin
git reset --hard origin/main

# Continue work on the rescue branch
git checkout "agent/rescue-..."   # use the name printed above

If the push already happened:

  • main is protected: your push was rejected, so just delete the local commits with the reset above. This is the case branch protection is designed to produce.
  • main is unprotected and the push succeeded: use git revert <oldest_bad>^..<newest_bad> to add reverting commits, then open a normal PR. Do not git push --force on main to erase the commits; that rewrites shared history and breaks everyone else’s clone.

If a local commit landed in the wrong worktree, move it: from the correct worktree run git cherry-pick <sha>, then git reset --hard HEAD~1 in the wrong one.

How to confirm it’s fixed

  1. Run the task again and immediately check git rev-parse --abbrev-ref HEAD (or git worktree list) — it should be on agent/<task>, never main.
  2. After the agent commits, git log --all --oneline --decorate should show the commits only on the task branch.
  3. For cloud tasks, open the PR and confirm both the head and base branches are what you intended before merging.
  4. Audit recent runs for the same bug:
# Branches with an agent prefix, oldest first — any with commits but no PR is suspect
git for-each-ref --format='%(refname:short) %(committerdate:iso8601)' \
  refs/remotes/origin/agent/ | sort -k2 | head -50

When it is not on you

If a self-hosted or vendor harness reuses worker pods without resetting the worktree, AGENTS.md rules and pre-push hooks cannot save you — the underlying state still leaks. File a bug with the harness owner; the fix has to be in the worker lifecycle. The local UI-vs-workdir mismatch (#23140 / #21712) is likewise an upstream bug; until it ships a fix, rely on explicit worktrees and the HEAD check in Step 7.

Prevention checklist

  • Cloud: set the base branch on every task instead of trusting the repo default
  • Local: one Git worktree per task; confirm with git worktree list
  • Setup script resets to origin/main and creates a per-task branch with a timestamp suffix
  • TASK_ID and any branch-name input is :?-guarded so empty values abort
  • AGENTS.md forbids git checkout main and direct commits to protected branches
  • Server-side branch protection on every protected branch pattern
  • Pre-push hook in the sandbox refuses pushes to protected refs
  • Commit wrapper verifies HEAD matches agent/*
  • Weekly audit of agent/* branches with no PR

FAQ

  • Why did my cloud task land on main instead of a branch? Cloud tasks check out the repo’s default branch unless you pick a base, and the head branch is only created when you open a PR. Set the base branch on the task and click Create PR. As of June 2026 the default really is your repo’s default branch.
  • @codex fix edited the wrong branch — why? Comment on the PR itself, not on an issue or the repo. From an issue, Codex branches off the default branch; from a PR it works against that PR’s branch.
  • The UI shows my branch but commits are missing. Where did they go? The local app can run tool calls in a different effective workdir than the prompt shows (#23140). Run git worktree list and git log --all --oneline to find them, then cherry-pick into the right worktree.
  • Can I let the agent reuse one branch across tasks for context? No. Cross-task state leaks both ways — task A’s code shows up in task B’s PR, and reviewers cannot read the agent’s intent from the diff.
  • My project merges feature branches into a long-lived integration branch. How do I protect it? Treat the integration branch like main: protect it, forbid direct commits, and require PRs even from the agent.

Tags: #Codex #AI coding #Troubleshooting