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

Codex ran git commit on whatever branch was checked out — sometimes main, sometimes a leftover branch from the previous task. How to force a clean per-task branch.

You give Codex a small task — “rename this prop, update call sites.” Twenty minutes later you check the PR list and there is none. You check git log on main: the agent’s commits are sitting on top of main, no branch, no PR. Or it pushed to feature/old-experiment from a teammate’s task three weeks ago. Or it created a new branch but checked out the wrong one before the final commit, so half the work landed on agent/task-7 and half on agent/task-8.

Codex inherits whatever branch the sandbox starts on. If the harness does not pin a fresh branch per task, every assumption downstream — PR creation, branch protection, review — silently breaks. Your “isolated agent run” turned out to be sharing state with the last one.

This article covers why the branch ends up wrong, how to force per-task isolation, and what to do when the bad commits have already landed.

Common causes

1. Sandbox starts on whatever branch was checked out last

A worker container is reused. The previous task left it on agent/task-42. The new task starts, Codex sees the repo, edits files, commits. The commits land on agent/task-42, not on a fresh branch tied to the current task.

How to spot it: PR is missing. git log --all --oneline shows your task’s commits sitting on a branch named after a previous task. Worker reuse is the source.

2. AGENTS.md says “commit on the current branch”

A well-meaning instruction told the agent to “commit your changes” without specifying which branch. The agent reads that literally and uses HEAD. If HEAD is main, your work lands on main.

How to spot it: grep -i 'commit on\|push to' AGENTS.md docs/. If there is no explicit “create a new branch named X-Y-Z first” rule, the agent will not.

3. git checkout -b failed silently and the agent kept going

The harness tried git checkout -b agent/fix-issue-123 but a branch with that name already existed from a previous run. git checkout -b errored. The agent ignored the error (it only inspects stdout) and proceeded on the still-current branch.

How to spot it: Transcript shows fatal: A branch named 'agent/fix-issue-123' already exists. followed by commits anyway. The commits went to whatever HEAD pointed at.

4. Agent rebased onto main, then committed on main directly

Codex was told “stay in sync with main.” It ran git checkout main && git pull, then forgot to switch back to the task branch before editing files. New commits land on main locally; if the harness then pushes main, it tries to push to a protected branch (best case) or succeeds (worst case).

How to spot it: Transcript contains git checkout main with no matching git checkout agent/... afterward. Commits’ parent SHAs sit on main’s tip.

5. Two parallel agent runs share the same worktree

You triggered two tasks at once. Both got worker pods that mounted the same persistent volume. They raced on git checkout -b, one succeeded, both committed. The loser’s commits ended up mixed onto the winner’s branch.

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

6. Branch name templated from an unset variable

The harness computes branch name as something like agent/${TASK_ID}. TASK_ID was empty. The agent ran git checkout -b agent/ which either errored or created a literal branch named agent/ depending on git version.

How to spot it: A branch literally named agent/, agent, or ${TASK_ID} shows up in git branch -a. Templating is the source.

Shortest path to fix

Step 1: Force a fresh branch at sandbox start

In .codex/setup.sh or your harness’s task-init hook:

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

# Reset to clean state, no matter what 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
BRANCH="agent/${TASK_ID:?TASK_ID must be set}-$(date +%s)"
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 if the same task is retried.

Step 2: Pin the branch in AGENTS.md

Add an explicit rule, near the top:

## 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/*.
- If you need to sync with main, use `git merge origin/main` from the task branch.

Codex will defer to the most explicit instruction it sees. Making this a top-of-file rule trumps the implicit “commit your changes” interpretation.

Step 3: Block direct pushes to main on the server

Repo settings on GitHub or your git host:

  • Branch protection on main, master, develop, all release/*
  • Block direct pushes (only via PR)
  • Block force-push
  • Require status checks

This is the safety net for when the previous steps fail. Even if Codex tries to push to main, the server refuses.

Step 4: Pre-push hook that refuses protected branches

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

This catches the failure at the sandbox layer, before the server even sees the push attempt — useful because the server-side rejection sometimes leaves the sandbox in a confusing half-state.

Step 5: Verify the branch in the commit step

Wrap the agent’s commit in a check:

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 fails loudly instead of landing somewhere wrong.

Step 6: Recovering when commits already landed wrong

If commits already went to main locally but were not pushed:

# Save the bad commits onto a new branch
git branch agent/rescue-$(date +%s)

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

# Continue work on the rescue branch
git checkout agent/rescue-...

If they already pushed to main and main is protected, your push was rejected — good, just delete the local commits as above. If main is unprotected and the push succeeded, you need git revert <range> plus a follow-up PR. Do not --force push main to wipe them; that breaks everyone else’s clone.

Step 7: Audit recent runs for the same bug

# Find branches that look orphaned (no PR, old commits, agent prefix)
git for-each-ref --format='%(refname:short) %(committerdate:iso8601)' refs/remotes/origin/agent/ \
  | sort -k2 \
  | head -50

Any branch with commits but no PR is suspect. Either the PR creation failed silently or the branch was wrong.

When this is not on you

If your harness vendor reuses worker pods without resetting the worktree, you can pin AGENTS.md and pre-push hooks all you want — the underlying state still leaks. File a bug with your Codex provider; the fix has to be in the worker lifecycle, not your repo.

Easy to misdiagnose as

“The PR creation step is flaky.” If there is no PR but there are commits, the PR step probably ran fine — it just had no new branch to open a PR against, because the commits landed somewhere unexpected. Look at branches before debugging the PR creator.

Prevention

  • Setup script resets to origin/main and creates a per-task branch with a timestamp suffix
  • TASK_ID and any other branch-name input is :?-guarded so empty values abort
  • AGENTS.md explicitly 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 the expected agent/* pattern
  • Weekly audit of orphan branches matching agent/* with no PR

FAQ

  • Can I let the agent reuse a branch across tasks for context? No. Cross-task state leaks both ways — code from task A appears in task B’s PR, and reviewers cannot tell the agent’s intent from the diff.
  • What if my project pushes from a feature branch into a long-lived integration branch? Treat the integration branch like main: protect it, forbid direct commits, require PRs even from the agent.

Tags: #Codex #AI coding #Troubleshooting