# git-auto-commit-action: commit back without loops or rejected pushes

> git-auto-commit-action commits a job's changes back to its branch. Get the v7 inputs, push permission, protected branch fixes and push loop guards.

Source: https://latchkey.dev/learn/actions/stefanzweifel-git-auto-commit-action  
Updated: 2026-11-12

`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](/learn/github-actions/stefanzweifel-git-auto-commit-nothing-to-commit).

```.github/workflows/format.yml
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](/learn/github-actions/github-actions-checkout-persisted-credentials-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` |

> Outputs: `changes_detected`, `commit_hash` and `create_git_tag_only`. From `action.yml` at v7.2.1, read 2026-11-12. The action needs bash on a UNIX-like runner.

## 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.

```.github/workflows/format.yml
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](/learn/actions/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](/learn/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`.

```.github/workflows/format.yml
concurrency:
  group: autocommit-${{ github.head_ref || github.ref }}
  cancel-in-progress: false
```

## v6 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 |

> From the release notes and UPGRADING.md on the action's repository and `entrypoint.sh` at v7.2.1, read 2026-11-12.

## FAQ

### Why do commits made by git-auto-commit not trigger new workflow runs?

Because the push used `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?

A rule that requires pull requests rejects any direct push, and the action does an ordinary `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?

The required checks ran on the commit before the action's. On the new head commit, a push made with `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?

Check the `.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.

## References

- [stefanzweifel/git-auto-commit-action README (read 2026-11-12)](https://github.com/stefanzweifel/git-auto-commit-action)
- [git-auto-commit-action entrypoint.sh at v7.2.1](https://github.com/stefanzweifel/git-auto-commit-action/blob/v7.2.1/entrypoint.sh)
- [Skipping workflow runs, docs.github.com](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/skip-workflow-runs)
- [Creating rulesets for a repository: bypass permissions](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository)
- [Issue #71: GH006 Protected branch update failed](https://github.com/stefanzweifel/git-auto-commit-action/issues/71)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
