git-auto-commit-action: commit back without loops or rejected pushes
stefanzweifel/git-auto-commit-action commits whatever the job changed and pushes it back to the branch the workflow ran on, which is how formatters and generators fix a pull request in place. It needs contents: write, it cannot push to a branch that requires pull requests, and with any token other than GITHUB_TOKEN its own commit starts the workflow again.
A formatter runs, files change, and you want them in the branch rather than in a failed check. That is the 80% case git-auto-commit-action names in its README, and it handles it in one step. The other 20% is where the questions come from: why nothing was committed, why the push was rejected, why CI did not run on the new commit, and how to stop a workflow that commits from triggering itself over and over.
Every behavior described below is read from entrypoint.sh at v7.2.1, the current release, on 2026-11-12, because that shell script, not the README, decides when the action commits and when it pushes.

What one run does
The action looks for changes with git status -s, limited to file_pattern (default ., the whole repository) and status_options. A clean result prints Working tree clean. Nothing to commit., sets changes_detected to false and ends green. A dirty one runs git fetch --depth=1, stages the pattern with git add, and checks again with git diff --staged, because a change that is only line endings shows up in the first check and not the second.
Then it commits as github-actions[bot] with the triggering user as author, tags if you asked for a tag, and pushes. When branch is set, and it defaults to github.head_ref, the pull request's branch, the push is git push --set-upstream origin HEAD:<branch> --atomic; on a push event that value is empty and it runs a plain git push origin.
The clean-tree case, including the near-identical message git itself prints, is taken apart on git-auto-commit nothing to commit.
name: Format
on:
pull_request:
permissions:
contents: write
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.head_ref }}
- run: npx prettier --write .
- id: commit
uses: stefanzweifel/git-auto-commit-action@d0b01107b5db882071f96de5dd4b54961ea93f4c # v7.2.1
with:
commit_message: 'style: apply prettier'
file_pattern: '*.ts *.tsx *.md'
- if: steps.commit.outputs.changes_detected == 'true'
env:
SHA: ${{ steps.commit.outputs.commit_hash }}
run: echo "Pushed $SHA"The token checkout leaves is the token that pushes
The push uses whatever credential actions/checkout left in the git config, so the token you give checkout is the token that pushes. With GITHUB_TOKEN that means permissions: contents: write; without it the push is refused with a message naming github-actions[bot], which checkout persisted credentials push 403 walks through. On pull_request, also check out ref: ${{ github.head_ref }}, or the job starts on a detached merge commit.
| Input | Default | Notes |
|---|---|---|
commit_message | Apply automatic changes | |
branch | ${{ github.head_ref }} | Needed on pull_request; empty on push |
file_pattern | . | Space separated pathspecs, used by both the dirty check and git add |
repository | . | Path to the repository under the workspace |
commit_options, add_options, status_options, push_options | empty | Passed through to git, for example --no-verify |
commit_user_name, commit_user_email | github-actions[bot] and its noreply address | The committer |
commit_author | the triggering actor | The author |
tag_name, tagging_message | empty | Create and push a tag with the commit |
skip_dirty_check, skip_fetch, skip_checkout, skip_push | false | skip_push added in v7.1.0 |
create_branch | false | Create branch locally and remotely if it is missing |
create_git_tag_only | false | Tag and push without committing |
disable_globbing | false | Stop the shell expanding file_pattern |
before_*_hook, after_*_hook | empty | Eight shell hooks around add, commit, tag and push; added in v7.2.0 |
disable_pull_request_target_trigger_warning | false | Silence the v7.2.0 warning on pull_request_target |
The push loop, and how to avoid it
With GITHUB_TOKEN there is no automatic loop, because GitHub does not start workflows from events that token causes. On a pull request branch the push still counts as a synchronize, and docs.github.com now says those pull_request runs are created in an approval-required state: they wait until someone with write access selects Approve workflows to run. push workflows do not run on the new commit at all, so a required check that comes from a push workflow waits for a status that never arrives.
Switch to a personal access token or an App token, which you do precisely to get CI on that commit, and the commit starts the workflow again. Usually the second run finds nothing to change and stops there. It loops when the job writes something different every run: a timestamp, a build number, a generated file with a date in it.
Three guards, strongest first. Make the job's output deterministic, so the second run is clean. Gate the job on the actor, as below, which stops the formatter from reacting to its own commit while the rest of CI still runs on it. Or add [skip ci] to commit_message; GitHub's documentation warns that a skipped workflow leaves its required checks pending, so a protected pull request cannot merge on that commit.
jobs:
format:
if: github.actor != 'my-format-bot[bot]'
runs-on: ubuntu-latest
steps:
- id: app
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: write
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.head_ref }}
token: ${{ steps.app.outputs.token }}
- run: npx prettier --write .
- uses: stefanzweifel/git-auto-commit-action@d0b01107b5db882071f96de5dd4b54961ea93f4c # v7.2.1
with:
commit_message: 'style: apply prettier'Protected branches and rulesets
A branch rule or ruleset that requires pull requests rejects the push outright, with the message in the title of issue #71: remote: error: GH006: Protected branch update failed for refs/heads/master. The action does a normal push, so allowing force pushes does not help, and the maintainer's answer in that issue is that the action cannot get around branch protection.
Two designs work. Push to the pull request branch instead of the protected one, which is what happens by default on pull_request: the fix lands in the pull request and goes through the same review. Or, when the job runs on the protected branch itself, open a pull request with peter-evans/create-pull-request instead of pushing.
If a direct push really is the design, rulesets let you put a GitHub App on the bypass list. Push with that App's token from actions/create-github-app-token and its push is exempt, while people still go through review.
Two other rejected pushes
! [rejected] main -> main (non-fast-forward) means someone pushed to the branch after your job checked it out. The README lists this as a known limitation: the action never pulls before pushing, and matrix jobs that each commit run into it. The action leaves pulling to you, so serialize the writers with a concurrency group: a queued job checks out only after the one ahead of it has pushed.
A commit that touches .github/workflows/ fails with refusing to allow a GitHub App to create or update workflow followed by the file and without workflows permission, as in issue #322. The workflow permissions: key has no workflows scope, so GITHUB_TOKEN cannot get it; use an App token with permission-workflows: write, or keep workflow files out of file_pattern.
concurrency:
group: autocommit-${{ github.head_ref || github.ref }}
cancel-in-progress: falsev6 failed on a detached HEAD; v7 logs it
On v7.2.1 a detached HEAD logs Repository is in a detached HEAD state. git-auto-commit will likely handle this automatically. To avoid it, check out a branch using the ref option in actions/checkout. where v6 failed the step.
v7.2.0 also prints a warning whenever it runs on pull_request_target, because checking out and running a fork's code with a write token and secrets is how repositories get compromised. The README suggests running fixers on push to the default branch instead.
| Release | Change |
|---|---|
| v5.0.0 | Node 20 |
| v6.0.0 | Fails early on a detached HEAD; removed create_branch, skip_fetch and skip_checkout |
| v7.0.0 | Node 24; the three removed inputs restored; tag_name added beside tagging_message |
| v7.1.0 | skip_push |
| v7.2.0 | Eight hooks; a warning on pull_request_target |
| v7.2.1 | Accepts repositories where .git is a file |
Key takeaways
- Two checks can end the run green with nothing pushed:
git statusonfile_pattern, thengit diff --staged. - The token given to
actions/checkoutis the token that pushes;GITHUB_TOKENneedscontents: write. - A PAT or App token push re-triggers the workflow; deterministic output or an actor gate stops the loop.
- Branch protection cannot be bypassed by the action; push to the PR branch, open a PR, or use a ruleset bypass App.
Frequently asked questions
Why do commits made by git-auto-commit not trigger new workflow runs?
GITHUB_TOKEN, and GitHub does not start workflows from that token's events, to prevent recursive runs. On a pull request branch the pull_request runs are created but wait for someone with write access to approve them; push workflows do not run. Give actions/checkout an App token or a personal access token and the push triggers workflows normally, which is also when you need a guard against the push loop.Why is git-auto-commit not working on a protected branch?
git push, so it fails with GH006. Commit to the pull request branch, open a pull request with create-pull-request, or add a GitHub App to the ruleset bypass list and push with its token.Why does the check say Waiting for status to be reported after the action commits?
GITHUB_TOKEN starts no push workflow, and its pull_request runs sit in an approval-required state until someone with write access approves them. Approve the runs, or push with an App token so CI runs on the new commit by itself.Why is a change to a file not detected?
.gitignore that applies to the file first; the README names it as the usual cause. Then check that file_pattern matches the path the earlier step wrote, and whether the only change was line endings, which the staged diff check drops.