[BUG] Claude Code repeatedly fails to follow CLAUDE.md session branch + push discipline

Status Fixed / completed
Maintainer reply None cached
Activity 6 comments · opened Jun 7, 2026 · closed Jun 13, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Severity: High — causes lost/stranded work every session

Model: claude-sonnet-4-6

CLAUDE.md

Project: Private Flutter game repo with a CLAUDE.md that defines mandatory git workflow

CLAUDE.md instructs Claude to:

Read CLAUDE.md at session start
Cut a new branch from master immediately
Rename the branch with a human-readable slug before first commit
Commit changes as work progresses
What actually happened (this session):

Claude made code changes to stats_screen.dart and design.md without first cutting a branch
When asked "you didn't push again did you?", Claude confirmed nothing was pushed — but then only checked git status, did not push
User had to explicitly call out that CLAUDE.md was ignored entirely before Claude took corrective action
Even after corrective action, Claude committed but did not push — again
Root cause (as I understand it):
The CLAUDE.md says "Never create a PR, merge, or trigger a build unless the user explicitly asks." Claude appears to be over-applying this rule to git push as well, even though push is a prerequisite for a PR and is not the same as creating a PR. The instruction about not creating PRs without being asked does not say anything about not pushing. Claude is conflating the two.

Expected behavior:
After committing to a session branch, Claude should push the branch to remote automatically as part of normal workflow. The PR/merge/build gate should apply only to those specific actions, not to pushing the branch.

Actual behavior:
Claude commits locally and stops, leaving the branch unsynced to remote. The user must ask explicitly for a push every single session.

Workaround:
User must say "now push" after every commit. This has happened multiple times across sessions.

What Should Happen?

After committing to a session branch, Claude should push the branch to remote automatically as part of normal workflow. The PR/merge/build gate should apply only to those specific actions, not to pushing the branch.

Claude should always respect CLAUDE.md

Error Messages/Logs

Steps to Reproduce

Use the attached CLAUDE.md
Keep opening a new chat session for each independent piece of work.
Note carefully when the instructions in CLAUDE.md are ignored.

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

Claude 1.11187.4 (584005)

Platform

Other

Operating System

Windows

Terminal/Shell

Other

Additional Information

Using the windows app, not claude code from cmd line

View original on GitHub ↗

6 Comments

yurukusa · 2 months ago

Your root-cause read (push being over-conflated with "don't create a PR/merge") is plausible, but I think the reason it recurs every session is more structural and worth naming: CLAUDE.md is a prompt the model follows probabilistically, not a constraint the runtime enforces. Under a long multi-step task the "cut a branch / commit / push" steps compete with everything else in context, so some fraction of the time one of them gets dropped — and "push after commit" is an especially easy one to skip because nothing visibly breaks when it's missed. Rewording the instruction lowers the failure rate but can't take it to zero, which matches what you're seeing.

The reliable fix is to move the part you actually care about (a commit never being left unpushed) out of the prompt and into a hook, which runs deterministically every turn regardless of what the model "remembered". A Stop hook is the natural place — it fires when Claude finishes responding, so it catches the unpushed branch no matter which path produced the commit.

Here's one I just wrote and tested against a throwaway repo with a bare remote (cases: never-pushed branch → sets upstream + pushes; branch ahead of upstream → pushes; nothing to push → no-op; on master/main → does nothing; push fails e.g. no remote → exits cleanly so it never wedges your turn):

#!/usr/bin/env bash
# .claude/hooks/auto-push-session-branch.sh
# Stop hook: push the current session branch when it's ahead of its remote.
set -uo pipefail

git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0

branch=$(git symbolic-ref --quiet --short HEAD 2>/dev/null || echo "")
[ -z "$branch" ] && exit 0          # detached HEAD: do nothing

# Never auto-push default/protected branches. Edit for your repo.
case "$branch" in
  main|master|develop|release) exit 0 ;;
esac

push() { if "$@"; then return 0; else
  echo "[auto-push] '$branch' not pushed (no remote / auth / rejected). Push manually." >&2
  exit 0; fi; }

if git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
  ahead=$(git rev-list --count '@{u}..HEAD' 2>/dev/null || echo 0)
  [ "$ahead" -eq 0 ] && exit 0      # nothing unpushed
  push git push --quiet
  echo "[auto-push] pushed $ahead commit(s) on '$branch'." >&2
else
  git rev-parse --verify --quiet HEAD >/dev/null 2>&1 || exit 0
  push git push --quiet -u origin "$branch"
  echo "[auto-push] set upstream and pushed '$branch' to origin." >&2
fi
exit 0

Wire it in .claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/auto-push-session-branch.sh" }
        ]
      }
    ]
  }
}

chmod +x the script first. It deliberately refuses to touch main/master, and on any push failure it prints a note and exits 0 so it can't break the session.

Two honest caveats:

  • This pushes whatever is committed at the end of each turn, including work-in-progress commits. That's exactly the "commit-as-progress + push" workflow your CLAUDE.md describes, but if you'd rather only push right after a commit actually happens, use a PostToolUse hook matching Bash and gate on the command containing git commit instead of Stop. Stop is simpler and catches manual commits too.
  • It does not solve the "cut a branch from master before the first commit" half — that one genuinely needs the model to act with intent, so it stays in CLAUDE.md. The hook only guarantees that once a commit exists on a session branch, it reaches the remote.

None of this excuses the underlying behavior — more reliable instruction-following is on Anthropic's side — but the hook makes the lost-work symptom go away today instead of waiting on it.

cakrit · 2 months ago

@yurukusa I appreciate the wonderful response. I will try the hook suggestion.

caioribeiroclw-pixel · 2 months ago

One useful distinction here is enforcement vs observability.

The Stop hook suggestion above is the right kind of enforcement: don't rely on CLAUDE.md prose for "never leave work unpushed" if the runtime can check git state deterministically.

For debugging the product issue, I'd also want a tiny content-free checkpoint around the hook / rule boundary, something like:

{
  "rule_id": "git.branch-and-push-discipline",
  "rule_source": "project CLAUDE.md",
  "checkpoint": "before-first-edit | before-commit | stop-hook",
  "rule_loaded_or_reloaded": true,
  "plan_cited_rule": false,
  "git_state_seen": {
    "branch_created_before_edit": false,
    "commits_ahead_of_upstream": 1,
    "push_required": true
  },
  "gate_outcome": "blocked | corrected | missed",
  "violation_category": "loaded_not_applied | hook_absent | hook_failed | ambiguous_push_policy"
}

That would separate a few cases that currently look identical from the user's side:

  • the rule was never loaded/reloaded
  • it was loaded but not cited before the first edit
  • the hook caught the problem and corrected it
  • the hook was absent/failed
  • the wording around "push" was ambiguous and the model rationalized around it

No repo content or transcript text needs to be logged for that. The key bit is whether the branch/push rule was checked at the decision point, not just whether it existed somewhere in CLAUDE.md.

cakrit · 2 months ago

I have added both suggestions. I will circle back if/when the behavior is observed again.

cakrit · 2 months ago

I believe I am clear on the root cause. Working on multiple sequential tasks on the same session causes the agent to confuse previous permissions to merge and build, as applying to the entire session. In addition to the suggestions made here, I have since enforced a strict policy of one session per branch and PR, which has resulted in no more occurences of the issue. Based on how useful such a practice is to also reduce token use, I don't consider this bug worth following up on, any longer.

github-actions[bot] · 14 days ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.