Skip to content

Checkout credential cleanup is still a no-op: checkout persists via includeIf.gitdir, not include.path (follow-up to #1510) #1721

Description

@a-vasyukov

Summary

The fix for #1510 walks include.path entries of the local git config, but actions/checkout
does not use include.path in the normal (non-global) code path — it writes includeIf.gitdir:
entries. So replaceCheckoutCredentials still removes nothing, and the checkout credential
(the workflow GITHUB_TOKEN) stays active for every git operation in the job, overriding the
app token the action puts into the remote URL.

Observed with anthropics/claude-code-action@v1.0.199 and actions/checkout@v7.0.1.

Evidence

Job log of a real tag-mode run (@claude on an issue comment, use_commit_signing: false):

Configuring git authentication for non-signing mode
Configuring git user...
Removing existing git authentication headers...
No existing authentication headers to remove      <-- cleanup found nothing
Updating remote URL with authentication...
✓ Updated remote URL with authentication token

The agent then committed and pushed successfully — but under GITHUB_TOKEN, not under the app
token, which we could confirm from the outside: the push produced no pull_request: synchronize event, so neither CI nor our review workflow ran on the commit the agent added to
its own PR. GitHub suppresses events for actions performed with GITHUB_TOKEN, and that is only
consistent with the header winning over the credentials embedded in the remote URL.

For contrast, the same repository on actions/checkout@v5.1.0 (which still wrote the header into
.git/config) logged ✓ Removed existing authentication headers, and pushes from the agent did
raise synchronize — CI and review ran on bot-pushed commits. The regression appeared for us
exactly when Dependabot bumped checkout from v5.1.0 to v7.0.1.

Why the current cleanup misses it

replaceCheckoutCredentials (src/github/operations/git-config.ts, v1.0.199) unsets
http.<server>/.extraheader from the local config and then from every file listed in
git config --local --get-all include.path.

actions/checkout v7.0.1 (src/git-auth-helper.ts, configureToken) only writes include.path
in the globalConfig branch. In the branch that runs for a normal checkout it writes, into the
repository-local config:

const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
await this.git.config(hostIncludeKey, credentialsConfigPath)

const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)

plus equivalent container-path variants. None of these are include.path, so the loop iterates
over an empty list and the header survives.

Impact

Two shapes, depending on what the workflow grants GITHUB_TOKEN:

  1. contents: write granted — the push succeeds under GITHUB_TOKEN and silently raises no
    events. Downstream workflows never run on commits the agent adds to an existing PR, while the
    PR still shows the checks from the previous SHA, so it looks verified. This is the failure we
    hit; it is silent, which makes it worse than a hard error.
  2. contents: write not granted — the push fails with 403 Write access to repository not granted, which appears to be what configureGitAuth function incompatible with actions/checkout@v4.3.1,v5.0.1,v6 #907 describes.

Suggested fix

Collect every include target rather than just include.path, e.g.

git config --local --get-regexp '^include(If\..+)?\.path$'

and unset http.<server>/.extraheader from each referenced file (the same
git config --file <path> --unset-all call already used). Optionally also drop the
includeIf.gitdir:* keys themselves once their credential file no longer holds the header.

A regression test would need to cover the non-global branch specifically — the current one only
exercises include.path, which checkout uses solely for global config.

Workaround for others hitting this

Give actions/checkout a token that is not GITHUB_TOKEN (e.g. from
actions/create-github-app-token), so whichever credential wins is still one whose actions raise
events. persist-credentials: false also avoids the conflict, but it has separate reported
issues (#1236, #1711).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:permissionsbugSomething isn't workingp2Non-showstopper bug or popular feature request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions