Skip to content

Update actions/checkout action to v6#148

Open
renovate[bot] wants to merge 1 commit intomasterfrom
renovate/actions-checkout-6-x
Open

Update actions/checkout action to v6#148
renovate[bot] wants to merge 1 commit intomasterfrom
renovate/actions-checkout-6-x

Conversation

@renovate
Copy link
Contributor

@renovate renovate bot commented Dec 2, 2025

This PR contains the following updates:

Package Type Update Change
actions/checkout action major v4.3.1v6.0.2

Release Notes

actions/checkout (actions/checkout)

v6.0.2

Compare Source

v6.0.1

Compare Source

v6.0.0

Compare Source

v5.0.1

Compare Source

What's Changed

Full Changelog: actions/checkout@v5...v5.0.1

v5.0.0

Compare Source

What's Changed
⚠️ Minimum Compatible Runner Version

v2.327.1
Release Notes

Make sure your runner is updated to this version or newer to use this release.

Full Changelog: actions/checkout@v4...v5.0.0


Configuration

📅 Schedule: Branch creation - "before 3am on Monday" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@github-actions
Copy link

github-actions bot commented Dec 2, 2025

[puLL-Merge] - actions/checkout@v4.3.1..v6.0.0

Diff
diff --git .github/workflows/check-dist.yml .github/workflows/check-dist.yml
index 53902eeb9..db3e37f2b 100644
--- .github/workflows/check-dist.yml
+++ .github/workflows/check-dist.yml
@@ -24,10 +24,10 @@ jobs:
     steps:
       - uses: actions/checkout@v4.1.6
 
-      - name: Set Node.js 20.x
+      - name: Set Node.js 24.x
         uses: actions/setup-node@v4
         with:
-          node-version: 20.x
+          node-version: 24.x
 
       - name: Install dependencies
         run: npm ci
diff --git .github/workflows/test.yml .github/workflows/test.yml
index cde9f060e..7c47d7b6a 100644
--- .github/workflows/test.yml
+++ .github/workflows/test.yml
@@ -18,7 +18,7 @@ jobs:
     steps:
       - uses: actions/setup-node@v4
         with:
-          node-version: 20.x
+          node-version: 24.x
       - uses: actions/checkout@v4.1.6
       - run: npm ci
       - run: npm run build
@@ -302,12 +302,15 @@ jobs:
       # Clone this repo
       - name: Checkout
         uses: actions/checkout@v4.1.6
+        with:
+          path: actions-checkout
 
       # Basic checkout using git
       - name: Checkout basic
         id: checkout
-        uses: ./
+        uses: ./actions-checkout
         with:
+          path: cloned-using-local-action
           ref: test-data/v2/basic
 
       # Verify output
@@ -325,7 +328,3 @@ jobs:
             echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
             exit 1
           fi
-
-      # needed to make checkout post cleanup succeed
-      - name: Fix Checkout
-        uses: actions/checkout@v4.1.6
diff --git .github/workflows/update-main-version.yml .github/workflows/update-main-version.yml
index 7bec7d5a8..643b954e4 100644
--- .github/workflows/update-main-version.yml
+++ .github/workflows/update-main-version.yml
@@ -11,6 +11,7 @@ on:
         type: choice
         description: The major version to update
         options:
+          - v5
           - v4
           - v3
           - v2
diff --git CHANGELOG.md CHANGELOG.md
index baf5c2d7e..25befb782 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,5 +1,18 @@
 # Changelog
 
+## V6.0.0
+* Persist creds to a separate file by @ericsciple in https://github.com/actions/checkout/pull/2286
+* Update README to include Node.js 24 support details and requirements by @salmanmkc in https://github.com/actions/checkout/pull/2248
+
+## V5.0.1
+* Port v6 cleanup to v5 by @ericsciple in https://github.com/actions/checkout/pull/2301
+
+## V5.0.0
+* Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
+
+## V4.3.1
+* Port v6 cleanup to v4 by @ericsciple in https://github.com/actions/checkout/pull/2305
+
 ## V4.3.0
 * docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
 * Add internal repos for checking out multiple repositories by @mouismail in https://github.com/actions/checkout/pull/1977
diff --git README.md README.md
index 8969446c3..5ad476f49 100644
--- README.md
+++ README.md
@@ -1,6 +1,21 @@
 [![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml)
 
-# Checkout V4
+# Checkout v6
+
+## What's new
+
+- Updated `persist-credentials` to store the credentials under `$RUNNER_TEMP` instead of directly in the local git config.
+  - This requires a minimum Actions Runner version of [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) to access the persisted credentials for [Docker container action](https://docs.github.com/en/actions/tutorials/use-containerized-services/create-a-docker-container-action) scenarios.
+
+# Checkout v5
+
+## What's new
+
+- Updated to the node24 runtime
+  - This requires a minimum Actions Runner version of [v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) to run.
+
+
+# Checkout v4
 
 This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
 
@@ -36,7 +51,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 <!-- start usage -->
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v5
   with:
     # Repository name with owner. For example, actions/checkout
     # Default: ${{ github.repository }}
@@ -149,24 +164,33 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 # Scenarios
 
-- [Fetch only the root files](#Fetch-only-the-root-files)
-- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
-- [Fetch only a single file](#Fetch-only-a-single-file)
-- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
-- [Checkout a different branch](#Checkout-a-different-branch)
-- [Checkout HEAD^](#Checkout-HEAD)
-- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
-- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested)
-- [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
-- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
-- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
-- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
-- [Push a commit to a PR using the built-in token](#Push-a-commit-to-a-PR-using-the-built-in-token)
+- [Checkout V5](#checkout-v5)
+  - [What's new](#whats-new)
+- [Checkout V4](#checkout-v4)
+    - [Note](#note)
+- [What's new](#whats-new-1)
+- [Usage](#usage)
+- [Scenarios](#scenarios)
+  - [Fetch only the root files](#fetch-only-the-root-files)
+  - [Fetch only the root files and `.github` and `src` folder](#fetch-only-the-root-files-and-github-and-src-folder)
+  - [Fetch only a single file](#fetch-only-a-single-file)
+  - [Fetch all history for all tags and branches](#fetch-all-history-for-all-tags-and-branches)
+  - [Checkout a different branch](#checkout-a-different-branch)
+  - [Checkout HEAD^](#checkout-head)
+  - [Checkout multiple repos (side by side)](#checkout-multiple-repos-side-by-side)
+  - [Checkout multiple repos (nested)](#checkout-multiple-repos-nested)
+  - [Checkout multiple repos (private)](#checkout-multiple-repos-private)
+  - [Checkout pull request HEAD commit instead of merge commit](#checkout-pull-request-head-commit-instead-of-merge-commit)
+  - [Checkout pull request on closed event](#checkout-pull-request-on-closed-event)
+  - [Push a commit using the built-in token](#push-a-commit-using-the-built-in-token)
+  - [Push a commit to a PR using the built-in token](#push-a-commit-to-a-pr-using-the-built-in-token)
+- [Recommended permissions](#recommended-permissions)
+- [License](#license)
 
 ## Fetch only the root files
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v5
   with:
     sparse-checkout: .
 ```
@@ -174,7 +198,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch only the root files and `.github` and `src` folder
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v5
   with:
     sparse-checkout: |
       .github
@@ -184,7 +208,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch only a single file
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v5
   with:
     sparse-checkout: |
       README.md
@@ -194,7 +218,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch all history for all tags and branches
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v5
   with:
     fetch-depth: 0
 ```
@@ -202,7 +226,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout a different branch
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v5
   with:
     ref: my-branch
 ```
@@ -210,7 +234,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout HEAD^
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v5
   with:
     fetch-depth: 2
 - run: git checkout HEAD^
@@ -220,12 +244,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v4
+  uses: actions/checkout@v5
   with:
     path: main
 
 - name: Checkout tools repo
-  uses: actions/checkout@v4
+  uses: actions/checkout@v5
   with:
     repository: my-org/my-tools
     path: my-tools
@@ -236,10 +260,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v4
+  uses: actions/checkout@v5
 
 - name: Checkout tools repo
-  uses: actions/checkout@v4
+  uses: actions/checkout@v5
   with:
     repository: my-org/my-tools
     path: my-tools
@@ -250,12 +274,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v4
+  uses: actions/checkout@v5
   with:
     path: main
 
 - name: Checkout private tools
-  uses: actions/checkout@v4
+  uses: actions/checkout@v5
   with:
     repository: my-org/my-private-tools
     token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
@@ -268,7 +292,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout pull request HEAD commit instead of merge commit
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v5
   with:
     ref: ${{ github.event.pull_request.head.sha }}
 ```
@@ -284,7 +308,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
 ```
 
 ## Push a commit using the built-in token
@@ -295,7 +319,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
       - run: |
           date > generated.txt
           # Note: the following account information will not work on GHES
@@ -317,7 +341,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v5
         with:
           ref: ${{ github.head_ref }}
       - run: |
diff --git __test__/git-auth-helper.test.ts __test__/git-auth-helper.test.ts
index 7633704cc..ad3566ad6 100644
--- __test__/git-auth-helper.test.ts
+++ __test__/git-auth-helper.test.ts
@@ -86,16 +86,29 @@ describe('git-auth-helper tests', () => {
     // Act
     await authHelper.configureAuth()
 
-    // Assert config
-    const configContent = (
+    // Assert config - check that .git/config contains includeIf entries
+    const localConfigContent = (
       await fs.promises.readFile(localGitConfigPath)
     ).toString()
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:')
+    ).toBeGreaterThanOrEqual(0)
+
+    // Assert credentials config file contains the actual credentials
+    const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(1)
+    const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+    const credentialsContent = (
+      await fs.promises.readFile(credentialsConfigPath)
+    ).toString()
     const basicCredential = Buffer.from(
       `x-access-token:${settings.authToken}`,
       'utf8'
     ).toString('base64')
     expect(
-      configContent.indexOf(
+      credentialsContent.indexOf(
         `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
       )
     ).toBeGreaterThanOrEqual(0)
@@ -120,7 +133,7 @@ describe('git-auth-helper tests', () => {
     'inject https://github.com as github server url'
   it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
     await testAuthHeader(
-      configureAuth_AcceptsGitHubServerUrl,
+      configureAuth_AcceptsGitHubServerUrlSetToGHEC,
       'https://github.com'
     )
   })
@@ -141,12 +154,17 @@ describe('git-auth-helper tests', () => {
       // Act
       await authHelper.configureAuth()
 
-      // Assert config
-      const configContent = (
-        await fs.promises.readFile(localGitConfigPath)
+      // Assert config - check credentials config file (not local .git/config)
+      const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+        f => f.startsWith('git-credentials-') && f.endsWith('.config')
+      )
+      expect(credentialsFiles.length).toBe(1)
+      const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+      const credentialsContent = (
+        await fs.promises.readFile(credentialsConfigPath)
       ).toString()
       expect(
-        configContent.indexOf(
+        credentialsContent.indexOf(
           `http.https://github.com/.extraheader AUTHORIZATION`
         )
       ).toBeGreaterThanOrEqual(0)
@@ -251,13 +269,16 @@ describe('git-auth-helper tests', () => {
       expectedSshCommand
     )
 
-    // Asserty git config
+    // Assert git config
     const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
       .toString()
       .split('\n')
       .filter(x => x)
-    expect(gitConfigLines).toHaveLength(1)
-    expect(gitConfigLines[0]).toMatch(/^http\./)
+    // Should have includeIf entries pointing to credentials file
+    expect(gitConfigLines.length).toBeGreaterThan(0)
+    expect(
+      gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
+    ).toBeTruthy()
   })
 
   const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
@@ -419,8 +440,20 @@ describe('git-auth-helper tests', () => {
     expect(
       configContent.indexOf('value-from-global-config')
     ).toBeGreaterThanOrEqual(0)
+    // Global config should have include.path pointing to credentials file
+    expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
+
+    // Check credentials in the separate config file
+    const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBeGreaterThan(0)
+    const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+    const credentialsContent = (
+      await fs.promises.readFile(credentialsConfigPath)
+    ).toString()
     expect(
-      configContent.indexOf(
+      credentialsContent.indexOf(
         `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
       )
     ).toBeGreaterThanOrEqual(0)
@@ -463,8 +496,20 @@ describe('git-auth-helper tests', () => {
       const configContent = (
         await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
       ).toString()
+      // Global config should have include.path pointing to credentials file
+      expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
+
+      // Check credentials in the separate config file
+      const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+        f => f.startsWith('git-credentials-') && f.endsWith('.config')
+      )
+      expect(credentialsFiles.length).toBeGreaterThan(0)
+      const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+      const credentialsContent = (
+        await fs.promises.readFile(credentialsConfigPath)
+      ).toString()
       expect(
-        configContent.indexOf(
+        credentialsContent.indexOf(
           `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
         )
       ).toBeGreaterThanOrEqual(0)
@@ -550,15 +595,15 @@ describe('git-auth-helper tests', () => {
       await authHelper.configureSubmoduleAuth()
 
       // Assert
-      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
+      // Should configure insteadOf (2 calls for two values)
+      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
         /unset-all.*insteadOf/
       )
-      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
-      expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
+      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
         /url.*insteadOf.*git@github.com:/
       )
-      expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
+      expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
         /url.*insteadOf.*org-123456@github.com:/
       )
     }
@@ -589,12 +634,12 @@ describe('git-auth-helper tests', () => {
       await authHelper.configureSubmoduleAuth()
 
       // Assert
-      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
+      // Should configure sshCommand (1 call)
+      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
         /unset-all.*insteadOf/
       )
-      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
-      expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
+      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
     }
   )
 
@@ -660,19 +705,201 @@ describe('git-auth-helper tests', () => {
     await setup(removeAuth_removesToken)
     const authHelper = gitAuthHelper.createAuthHelper(git, settings)
     await authHelper.configureAuth()
-    let gitConfigContent = (
+
+    // Verify includeIf entries exist in local config
+    let localConfigContent = (
       await fs.promises.readFile(localGitConfigPath)
     ).toString()
-    expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:')
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify both host and container includeIf entries are present
+    const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
+    expect(
+      localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify credentials file exists
+    let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(1)
+    const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
+
+    // Verify credentials file contains the auth token
+    let credentialsContent = (
+      await fs.promises.readFile(credentialsFilePath)
+    ).toString()
+    const basicCredential = Buffer.from(
+      `x-access-token:${settings.authToken}`,
+      'utf8'
+    ).toString('base64')
+    expect(
+      credentialsContent.indexOf(
+        `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
+      )
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify the includeIf entries point to the credentials file
+    const containerCredentialsPath = path.posix.join(
+      '/github/runner_temp',
+      path.basename(credentialsFilePath)
+    )
+    expect(
+      localConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      localConfigContent.indexOf(containerCredentialsPath)
+    ).toBeGreaterThanOrEqual(0)
 
     // Act
     await authHelper.removeAuth()
 
-    // Assert git config
-    gitConfigContent = (
+    // Assert all includeIf entries removed from local git config
+    localConfigContent = (
       await fs.promises.readFile(localGitConfigPath)
     ).toString()
-    expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
+    expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+    expect(
+      localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
+    ).toBeLessThan(0)
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
+    ).toBeLessThan(0)
+    expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+    expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+    // Assert credentials config file deleted
+    credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(0)
+
+    // Verify credentials file no longer exists on disk
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
+  })
+
+  const removeAuth_removesTokenFromSubmodules =
+    'removeAuth removes token from submodules'
+  it(removeAuth_removesTokenFromSubmodules, async () => {
+    // Arrange
+    await setup(removeAuth_removesTokenFromSubmodules)
+
+    // Create fake submodule config paths
+    const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
+    const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2')
+    const submodule1ConfigPath = path.join(submodule1Dir, 'config')
+    const submodule2ConfigPath = path.join(submodule2Dir, 'config')
+
+    await fs.promises.mkdir(submodule1Dir, {recursive: true})
+    await fs.promises.mkdir(submodule2Dir, {recursive: true})
+    await fs.promises.writeFile(submodule1ConfigPath, '')
+    await fs.promises.writeFile(submodule2ConfigPath, '')
+
+    // Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
+    const mockGetSubmoduleConfigPaths =
+      git.getSubmoduleConfigPaths as jest.Mock<any, any>
+    mockGetSubmoduleConfigPaths.mockResolvedValue([
+      submodule1ConfigPath,
+      submodule2ConfigPath
+    ])
+
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+    await authHelper.configureSubmoduleAuth()
+
+    // Verify credentials file exists
+    let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(1)
+    const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
+
+    // Verify submodule 1 config has includeIf entries
+    let submodule1Content = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
+    expect(
+      submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      submodule1Content.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify submodule 2 config has includeIf entries
+    let submodule2Content = (
+      await fs.promises.readFile(submodule2ConfigPath)
+    ).toString()
+    const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
+    expect(
+      submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      submodule2Content.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify both host and container paths are in each submodule config
+    const containerCredentialsPath = path.posix.join(
+      '/github/runner_temp',
+      path.basename(credentialsFilePath)
+    )
+    expect(
+      submodule1Content.indexOf(containerCredentialsPath)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      submodule2Content.indexOf(containerCredentialsPath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Act - ensure mock persists for removeAuth
+    mockGetSubmoduleConfigPaths.mockResolvedValue([
+      submodule1ConfigPath,
+      submodule2ConfigPath
+    ])
+    await authHelper.removeAuth()
+
+    // Assert submodule 1 includeIf entries removed
+    submodule1Content = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+    expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
+    expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+    // Assert submodule 2 includeIf entries removed
+    submodule2Content = (
+      await fs.promises.readFile(submodule2ConfigPath)
+    ).toString()
+    expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+    expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0)
+    expect(submodule2Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+    // Assert credentials config file deleted
+    credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(0)
+
+    // Verify credentials file no longer exists on disk
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
   })
 
   const removeGlobalConfig_removesOverride =
@@ -701,6 +928,52 @@ describe('git-auth-helper tests', () => {
       }
     }
   })
+
+  const testCredentialsConfigPath_matchesCredentialsConfigPaths =
+    'testCredentialsConfigPath matches credentials config paths'
+  it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => {
+    // Arrange
+    await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths)
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+
+    // Get a real credentials config path
+    const credentialsConfigPath = await (
+      authHelper as any
+    ).getCredentialsConfigPath()
+
+    // Act & Assert
+    expect(
+      (authHelper as any).testCredentialsConfigPath(credentialsConfigPath)
+    ).toBe(true)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config'
+      )
+    ).toBe(true)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
+      )
+    ).toBe(true)
+
+    // Test invalid paths
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/other-config.config'
+      )
+    ).toBe(false)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-invalid.config'
+      )
+    ).toBe(false)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-.config'
+      )
+    ).toBe(false)
+    expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
+  })
 })
 
 async function setup(testName: string): Promise<void> {
@@ -715,6 +988,7 @@ async function setup(testName: string): Promise<void> {
   await fs.promises.mkdir(tempHomedir, {recursive: true})
   process.env['RUNNER_TEMP'] = runnerTemp
   process.env['HOME'] = tempHomedir
+  process.env['GITHUB_WORKSPACE'] = workspace
 
   // Create git config
   globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
@@ -733,10 +1007,20 @@ async function setup(testName: string): Promise<void> {
     checkout: jest.fn(),
     checkoutDetach: jest.fn(),
     config: jest.fn(
-      async (key: string, value: string, globalConfig?: boolean) => {
-        const configPath = globalConfig
-          ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
-          : localGitConfigPath
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        add?: boolean,
+        configFile?: string
+      ) => {
+        const configPath =
+          configFile ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        // Ensure directory exists
+        await fs.promises.mkdir(path.dirname(configPath), {recursive: true})
         await fs.promises.appendFile(configPath, `\n${key} ${value}`)
       }
     ),
@@ -756,6 +1040,7 @@ async function setup(testName: string): Promise<void> {
     env: {},
     fetch: jest.fn(),
     getDefaultBranch: jest.fn(),
+    getSubmoduleConfigPaths: jest.fn(async () => []),
     getWorkingDirectory: jest.fn(() => workspace),
     init: jest.fn(),
     isDetached: jest.fn(),
@@ -794,8 +1079,72 @@ async function setup(testName: string): Promise<void> {
         return true
       }
     ),
+    tryConfigUnsetValue: jest.fn(
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ): Promise<boolean> => {
+        const targetConfigPath =
+          configPath ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        let content = await fs.promises.readFile(targetConfigPath)
+        let lines = content
+          .toString()
+          .split('\n')
+          .filter(x => x)
+          .filter(x => !(x.startsWith(key) && x.includes(value)))
+        await fs.promises.writeFile(targetConfigPath, lines.join('\n'))
+        return true
+      }
+    ),
     tryDisableAutomaticGarbageCollection: jest.fn(),
     tryGetFetchUrl: jest.fn(),
+    tryGetConfigValues: jest.fn(
+      async (
+        key: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ): Promise<string[]> => {
+        const targetConfigPath =
+          configPath ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        const content = await fs.promises.readFile(targetConfigPath)
+        const lines = content
+          .toString()
+          .split('\n')
+          .filter(x => x && x.startsWith(key))
+          .map(x => x.substring(key.length).trim())
+        return lines
+      }
+    ),
+    tryGetConfigKeys: jest.fn(
+      async (
+        pattern: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ): Promise<string[]> => {
+        const targetConfigPath =
+          configPath ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        const content = await fs.promises.readFile(targetConfigPath)
+        const lines = content
+          .toString()
+          .split('\n')
+          .filter(x => x)
+        const keys = lines
+          .filter(x => new RegExp(pattern).test(x.split(' ')[0]))
+          .map(x => x.split(' ')[0])
+        return [...new Set(keys)] // Remove duplicates
+      }
+    ),
     tryReset: jest.fn(),
     version: jest.fn()
   }
@@ -830,6 +1179,7 @@ async function setup(testName: string): Promise<void> {
 
 async function getActualSshKeyPath(): Promise<string> {
   let actualTempFiles = (await fs.promises.readdir(runnerTemp))
+    .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
     .sort()
     .map(x => path.join(runnerTemp, x))
   if (actualTempFiles.length === 0) {
@@ -843,6 +1193,7 @@ async function getActualSshKeyPath(): Promise<string> {
 
 async function getActualSshKnownHostsPath(): Promise<string> {
   let actualTempFiles = (await fs.promises.readdir(runnerTemp))
+    .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
     .sort()
     .map(x => path.join(runnerTemp, x))
   if (actualTempFiles.length === 0) {
diff --git __test__/git-directory-helper.test.ts __test__/git-directory-helper.test.ts
index 22e9ae6d4..de79dc890 100644
--- __test__/git-directory-helper.test.ts
+++ __test__/git-directory-helper.test.ts
@@ -471,6 +471,7 @@ async function setup(testName: string): Promise<void> {
     configExists: jest.fn(),
     fetch: jest.fn(),
     getDefaultBranch: jest.fn(),
+    getSubmoduleConfigPaths: jest.fn(async () => []),
     getWorkingDirectory: jest.fn(() => repositoryPath),
     init: jest.fn(),
     isDetached: jest.fn(),
@@ -493,12 +494,15 @@ async function setup(testName: string): Promise<void> {
       return true
     }),
     tryConfigUnset: jest.fn(),
+    tryConfigUnsetValue: jest.fn(),
     tryDisableAutomaticGarbageCollection: jest.fn(),
     tryGetFetchUrl: jest.fn(async () => {
       // Sanity check - this function shouldn't be called when the .git directory doesn't exist
       await fs.promises.stat(path.join(repositoryPath, '.git'))
       return repositoryUrl
     }),
+    tryGetConfigValues: jest.fn(),
+    tryGetConfigKeys: jest.fn(),
     tryReset: jest.fn(async () => {
       return true
     }),
diff --git __test__/verify-submodules-recursive.sh __test__/verify-submodules-recursive.sh
index 1b68f9b97..5ecbb42d0 100755
--- __test__/verify-submodules-recursive.sh
+++ __test__/verify-submodules-recursive.sh
@@ -17,7 +17,7 @@ fi
 
 echo "Testing persisted credential"
 pushd ./submodules-recursive/submodule-level-1/submodule-level-2
-git config --local --name-only --get-regexp http.+extraheader && git fetch
+git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
 if [ "$?" != "0" ]; then
     echo "Failed to validate persisted credential"
     popd
diff --git __test__/verify-submodules-true.sh __test__/verify-submodules-true.sh
index 43769fe06..4c311f846 100755
--- __test__/verify-submodules-true.sh
+++ __test__/verify-submodules-true.sh
@@ -17,7 +17,7 @@ fi
 
 echo "Testing persisted credential"
 pushd ./submodules-true/submodule-level-1
-git config --local --name-only --get-regexp http.+extraheader && git fetch
+git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
 if [ "$?" != "0" ]; then
     echo "Failed to validate persisted credential"
     popd
diff --git action.yml action.yml
index 6842eb843..767c41649 100644
--- action.yml
+++ action.yml
@@ -104,6 +104,6 @@ outputs:
   commit:
     description: 'The commit SHA that was checked out'
 runs:
-  using: node20
+  using: node24
   main: dist/index.js
   post: dist/index.js
diff --git dist/index.js dist/index.js
index f3ae6f3ea..a251a1966 100644
--- dist/index.js
+++ dist/index.js
@@ -162,6 +162,7 @@ class GitAuthHelper {
         this.sshKeyPath = '';
         this.sshKnownHostsPath = '';
         this.temporaryHomePath = '';
+        this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP
         this.git = gitCommandManager;
         this.settings = gitSourceSettings || {};
         // Token auth header
@@ -229,15 +230,17 @@ class GitAuthHelper {
     configureGlobalAuth() {
         return __awaiter(this, void 0, void 0, function* () {
             // 'configureTempGlobalConfig' noops if already set, just returns the path
-            const newGitConfigPath = yield this.configureTempGlobalConfig();
+            yield this.configureTempGlobalConfig();
             try {
                 // Configure the token
-                yield this.configureToken(newGitConfigPath, true);
+                yield this.configureToken(true);
                 // Configure HTTPS instead of SSH
                 yield this.git.tryConfigUnset(this.insteadOfKey, true);
                 if (!this.settings.sshKey) {
                     for (const insteadOfValue of this.insteadOfValues) {
-                        yield this.git.config(this.insteadOfKey, insteadOfValue, true, true);
+                        yield this.git.config(this.insteadOfKey, insteadOfValue, true, // globalConfig?
+                        true // add?
+                        );
                     }
                 }
             }
@@ -252,19 +255,34 @@ class GitAuthHelper {
     configureSubmoduleAuth() {
         return __awaiter(this, void 0, void 0, function* () {
             // Remove possible previous HTTPS instead of SSH
-            yield this.removeGitConfig(this.insteadOfKey, true);
+            yield this.removeSubmoduleGitConfig(this.insteadOfKey);
             if (this.settings.persistCredentials) {
-                // Configure a placeholder value. This approach avoids the credential being captured
-                // by process creation audit events, which are commonly logged. For more information,
-                // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
-                const output = yield this.git.submoduleForeach(
-                // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
-                `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules);
-                // Replace the placeholder
-                const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+                // Get the credentials config file path in RUNNER_TEMP
+                const credentialsConfigPath = this.getCredentialsConfigPath();
+                // Container credentials config path
+                const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
+                // Get submodule config file paths.
+                const configPaths = yield this.git.getSubmoduleConfigPaths(this.settings.nestedSubmodules);
+                // For each submodule, configure includeIf entries pointing to the shared credentials file.
+                // Configure both host and container paths to support Docker container actions.
                 for (const configPath of configPaths) {
-                    core.debug(`Replacing token placeholder in '${configPath}'`);
-                    yield this.replaceTokenPlaceholder(configPath);
+                    // Submodule Git directory
+                    let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
+                    submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                    // Configure host includeIf
+                    yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
+                    false, // add?
+                    configPath);
+                    // Container submodule git directory
+                    const githubWorkspace = process.env['GITHUB_WORKSPACE'];
+                    assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
+                    let relativeSubmoduleGitDir = path.relative(githubWorkspace, submoduleGitDir);
+                    relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                    const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir);
+                    // Configure container includeIf
+                    yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
+                    false, // add?
+                    configPath);
                 }
                 if (this.settings.sshKey) {
                     // Configure core.sshCommand
@@ -295,6 +313,10 @@ class GitAuthHelper {
             }
         });
     }
+    /**
+     * Configures SSH authentication by writing the SSH key and known hosts,
+     * and setting up the GIT_SSH_COMMAND environment variable.
+     */
     configureSsh() {
         return __awaiter(this, void 0, void 0, function* () {
             if (!this.settings.sshKey) {
@@ -351,43 +373,88 @@ class GitAuthHelper {
             }
         });
     }
-    configureToken(configPath, globalConfig) {
-        return __awaiter(this, void 0, void 0, function* () {
-            // Validate args
-            assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations');
-            // Default config path
-            if (!configPath && !globalConfig) {
-                configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config');
-            }
-            // Configure a placeholder value. This approach avoids the credential being captured
-            // by process creation audit events, which are commonly logged. For more information,
-            // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
-            yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig);
-            // Replace the placeholder
-            yield this.replaceTokenPlaceholder(configPath || '');
-        });
-    }
-    replaceTokenPlaceholder(configPath) {
+    /**
+     * Configures token-based authentication by creating a credentials config file
+     * and setting up includeIf entries to reference it.
+     * @param globalConfig Whether to configure global config instead of local
+     */
+    configureToken(globalConfig) {
         return __awaiter(this, void 0, void 0, function* () {
-            assert.ok(configPath, 'configPath is not defined');
-            let content = (yield fs.promises.readFile(configPath)).toString();
+            // Get the credentials config file path in RUNNER_TEMP
+            const credentialsConfigPath = this.getCredentialsConfigPath();
+            // Write placeholder to the separate credentials config file using git config.
+            // This approach avoids the credential being captured by process creation audit events,
+            // which are commonly logged. For more information, refer to
+            // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
+            yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, false, // globalConfig?
+            false, // add?
+            credentialsConfigPath);
+            // Replace the placeholder in the credentials config file
+            let content = (yield fs.promises.readFile(credentialsConfigPath)).toString();
             const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
             if (placeholderIndex < 0 ||
                 placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) {
-                throw new Error(`Unable to replace auth placeholder in ${configPath}`);
+                throw new Error(`Unable to replace auth placeholder in ${credentialsConfigPath}`);
             }
             assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
             content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue);
-            yield fs.promises.writeFile(configPath, content);
+            yield fs.promises.writeFile(credentialsConfigPath, content);
+            // Add include or includeIf to reference the credentials config
+            if (globalConfig) {
+                // Global config file is temporary
+                yield this.git.config('include.path', credentialsConfigPath, true // globalConfig?
+                );
+            }
+            else {
+                // Host git directory
+                let gitDir = path.join(this.git.getWorkingDirectory(), '.git');
+                gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                // Configure host includeIf
+                const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
+                yield this.git.config(hostIncludeKey, credentialsConfigPath);
+                // Container git directory
+                const workingDirectory = this.git.getWorkingDirectory();
+                const githubWorkspace = process.env['GITHUB_WORKSPACE'];
+                assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
+                let relativePath = path.relative(githubWorkspace, workingDirectory);
+                relativePath = relativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git');
+                // Container credentials config path
+                const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
+                // Configure container includeIf
+                const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
+                yield this.git.config(containerIncludeKey, containerCredentialsPath);
+            }
         });
     }
+    /**
+     * Gets or creates the path to the credentials config file in RUNNER_TEMP.
+     * @returns The absolute path to the credentials config file
+     */
+    getCredentialsConfigPath() {
+        if (this.credentialsConfigPath) {
+            return this.credentialsConfigPath;
+        }
+        const runnerTemp = process.env['RUNNER_TEMP'] || '';
+        assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
+        // Create a unique filename for this checkout instance
+        const configFileName = `git-credentials-${(0, uuid_1.v4)()}.config`;
+        this.credentialsConfigPath = path.join(runnerTemp, configFileName);
+        core.debug(`Credentials config path: ${this.credentialsConfigPath}`);
+        return this.credentialsConfigPath;
+    }
+    /**
+     * Removes SSH authentication configuration by cleaning up SSH keys,
+     * known hosts files, and SSH command configurations.
+     */
     removeSsh() {
         return __awaiter(this, void 0, void 0, function* () {
-            var _a;
+            var _a, _b;
             // SSH key
             const keyPath = this.sshKeyPath || stateHelper.SshKeyPath;
             if (keyPath) {
                 try {
+                    core.info(`Removing SSH key '${keyPath}'`);
                     yield io.rmRF(keyPath);
                 }
                 catch (err) {
@@ -399,37 +466,136 @@ class GitAuthHelper {
             const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath;
             if (knownHostsPath) {
                 try {
+                    core.info(`Removing SSH known hosts '${knownHostsPath}'`);
                     yield io.rmRF(knownHostsPath);
                 }
-                catch (_b) {
-                    // Intentionally empty
+                catch (err) {
+                    core.debug(`${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
+                    core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`);
                 }
             }
             // SSH command
+            core.info('Removing SSH command configuration');
             yield this.removeGitConfig(SSH_COMMAND_KEY);
+            yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY);
         });
     }
+    /**
+     * Removes token-based authentication by cleaning up HTTP headers,
+     * includeIf entries, and credentials config files.
+     */
     removeToken() {
         return __awaiter(this, void 0, void 0, function* () {
-            // HTTP extra header
+            var _a;
+            // Remove HTTP extra header
+            core.info('Removing HTTP extra header');
             yield this.removeGitConfig(this.tokenConfigKey);
+            yield this.removeSubmoduleGitConfig(this.tokenConfigKey);
+            // Collect credentials config paths that need to be removed
+            const credentialsPaths = new Set();
+            // Remove includeIf entries that point to git-credentials-*.config files
+            core.info('Removing includeIf entries pointing to credentials config files');
+            const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
+            mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
+            // Remove submodule includeIf entries that point to git-credentials-*.config files
+            const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true);
+            for (const configPath of submoduleConfigPaths) {
+                const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath);
+                submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path));
+            }
+            // Remove credentials config files
+            for (const credentialsPath of credentialsPaths) {
+                // Only remove credentials config files if they are under RUNNER_TEMP
+                const runnerTemp = process.env['RUNNER_TEMP'];
+                assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
+                if (credentialsPath.startsWith(runnerTemp)) {
+                    try {
+                        core.info(`Removing credentials config '${credentialsPath}'`);
+                        yield io.rmRF(credentialsPath);
+                    }
+                    catch (err) {
+                        core.debug(`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
+                        core.warning(`Failed to remove credentials config '${credentialsPath}'`);
+                    }
+                }
+                else {
+                    core.debug(`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`);
+                }
+            }
         });
     }
-    removeGitConfig(configKey_1) {
-        return __awaiter(this, arguments, void 0, function* (configKey, submoduleOnly = false) {
-            if (!submoduleOnly) {
-                if ((yield this.git.configExists(configKey)) &&
-                    !(yield this.git.tryConfigUnset(configKey))) {
-                    // Load the config contents
-                    core.warning(`Failed to remove '${configKey}' from the git config`);
-                }
+    /**
+     * Removes a git config key from the local repository config.
+     * @param configKey The git config key to remove
+     */
+    removeGitConfig(configKey) {
+        return __awaiter(this, void 0, void 0, function* () {
+            if ((yield this.git.configExists(configKey)) &&
+                !(yield this.git.tryConfigUnset(configKey))) {
+                // Load the config contents
+                core.warning(`Failed to remove '${configKey}' from the git config`);
             }
+        });
+    }
+    /**
+     * Removes a git config key from all submodule configs.
+     * @param configKey The git config key to remove
+     */
+    removeSubmoduleGitConfig(configKey) {
+        return __awaiter(this, void 0, void 0, function* () {
             const pattern = regexpHelper.escape(configKey);
             yield this.git.submoduleForeach(
-            // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
+            // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
             `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
         });
     }
+    /**
+     * Removes includeIf entries that point to git-credentials-*.config files.
+     * @param configPath Optional path to a specific git config file to operate on
+     * @returns Array of unique credentials config file paths that were found and removed
+     */
+    removeIncludeIfCredentials(configPath) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const credentialsPaths = new Set();
+            try {
+                // Get all includeIf.gitdir keys
+                const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
+                configPath);
+                for (const key of keys) {
+                    // Get all values for this key
+                    const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
+                    configPath);
+                    if (values.length > 0) {
+                        // Remove only values that match git-credentials-<uuid>.config pattern
+                        for (const value of values) {
+                            if (this.testCredentialsConfigPath(value)) {
+                                credentialsPaths.add(value);
+                                yield this.git.tryConfigUnsetValue(key, value, false, configPath);
+                            }
+                        }
+                    }
+                }
+            }
+            catch (err) {
+                // Ignore errors - this is cleanup code
+                if (configPath) {
+                    core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`);
+                }
+                else {
+                    core.debug(`Error during includeIf cleanup: ${err}`);
+                }
+            }
+            return Array.from(credentialsPaths);
+        });
+    }
+    /**
+     * Tests if a path matches the git-credentials-*.config pattern.
+     * @param path The path to test
+     * @returns True if the path matches the credentials config pattern
+     */
+    testCredentialsConfigPath(path) {
+        return /git-credentials-[0-9a-f-]+\.config$/i.test(path);
+    }
 }
 
 
@@ -627,9 +793,15 @@ class GitCommandManager {
             yield this.execGit(args);
         });
     }
-    config(configKey, configValue, globalConfig, add) {
+    config(configKey, configValue, globalConfig, add, configFile) {
         return __awaiter(this, void 0, void 0, function* () {
-            const args = ['config', globalConfig ? '--global' : '--local'];
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
             if (add) {
                 args.push('--add');
             }
@@ -706,6 +878,16 @@ class GitCommandManager {
             throw new Error('Unexpected output when retrieving default branch');
         });
     }
+    getSubmoduleConfigPaths(recursive) {
+        return __awaiter(this, void 0, void 0, function* () {
+            // Get submodule config file paths.
+            // Use `--show-origin` to get the config file path for each submodule.
+            const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive);
+            // Extract config file paths from the output (lines starting with "file:").
+            const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+            return configPaths;
+        });
+    }
     getWorkingDirectory() {
         return this.workingDirectory;
     }
@@ -836,6 +1018,20 @@ class GitCommandManager {
             return output.exitCode === 0;
         });
     }
+    tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--unset', configKey, configValue);
+            const output = yield this.execGit(args, true);
+            return output.exitCode === 0;
+        });
+    }
     tryDisableAutomaticGarbageCollection() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
@@ -855,6 +1051,46 @@ class GitCommandManager {
             return stdout;
         });
     }
+    tryGetConfigValues(configKey, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--get-all', configKey);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(value => value.trim());
+        });
+    }
+    tryGetConfigKeys(pattern, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--name-only', '--get-regexp', pattern);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(key => key.trim());
+        });
+    }
     tryReset() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
diff --git package.json package.json
index dbbaabbae..4b2b58a51 100644
--- package.json
+++ package.json
@@ -1,6 +1,6 @@
 {
   "name": "checkout",
-  "version": "4.3.0",
+  "version": "5.0.0",
   "description": "checkout action",
   "main": "lib/main.js",
   "scripts": {
@@ -37,7 +37,7 @@
   },
   "devDependencies": {
     "@types/jest": "^29.5.12",
-    "@types/node": "^20.12.12",
+    "@types/node": "^24.1.0",
     "@types/uuid": "^9.0.8",
     "@typescript-eslint/eslint-plugin": "^7.9.0",
     "@typescript-eslint/parser": "^7.9.0",
diff --git src/git-auth-helper.ts src/git-auth-helper.ts
index 126e8e5ee..a1950a60c 100644
--- src/git-auth-helper.ts
+++ src/git-auth-helper.ts
@@ -43,6 +43,7 @@ class GitAuthHelper {
   private sshKeyPath = ''
   private sshKnownHostsPath = ''
   private temporaryHomePath = ''
+  private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
 
   constructor(
     gitCommandManager: IGitCommandManager,
@@ -126,16 +127,21 @@ class GitAuthHelper {
 
   async configureGlobalAuth(): Promise<void> {
     // 'configureTempGlobalConfig' noops if already set, just returns the path
-    const newGitConfigPath = await this.configureTempGlobalConfig()
+    await this.configureTempGlobalConfig()
     try {
       // Configure the token
-      await this.configureToken(newGitConfigPath, true)
+      await this.configureToken(true)
 
       // Configure HTTPS instead of SSH
       await this.git.tryConfigUnset(this.insteadOfKey, true)
       if (!this.settings.sshKey) {
         for (const insteadOfValue of this.insteadOfValues) {
-          await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
+          await this.git.config(
+            this.insteadOfKey,
+            insteadOfValue,
+            true, // globalConfig?
+            true // add?
+          )
         }
       }
     } catch (err) {
@@ -150,24 +156,60 @@ class GitAuthHelper {
 
   async configureSubmoduleAuth(): Promise<void> {
     // Remove possible previous HTTPS instead of SSH
-    await this.removeGitConfig(this.insteadOfKey, true)
+    await this.removeSubmoduleGitConfig(this.insteadOfKey)
 
     if (this.settings.persistCredentials) {
-      // Configure a placeholder value. This approach avoids the credential being captured
-      // by process creation audit events, which are commonly logged. For more information,
-      // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
-      const output = await this.git.submoduleForeach(
-        // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
-        `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
+      // Get the credentials config file path in RUNNER_TEMP
+      const credentialsConfigPath = this.getCredentialsConfigPath()
+
+      // Container credentials config path
+      const containerCredentialsPath = path.posix.join(
+        '/github/runner_temp',
+        path.basename(credentialsConfigPath)
+      )
+
+      // Get submodule config file paths.
+      const configPaths = await this.git.getSubmoduleConfigPaths(
         this.settings.nestedSubmodules
       )
 
-      // Replace the placeholder
-      const configPaths: string[] =
-        output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+      // For each submodule, configure includeIf entries pointing to the shared credentials file.
+      // Configure both host and container paths to support Docker container actions.
       for (const configPath of configPaths) {
-        core.debug(`Replacing token placeholder in '${configPath}'`)
-        await this.replaceTokenPlaceholder(configPath)
+        // Submodule Git directory
+        let submoduleGitDir = path.dirname(configPath) // The config file is at .git/modules/submodule-name/config
+        submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+
+        // Configure host includeIf
+        await this.git.config(
+          `includeIf.gitdir:${submoduleGitDir}.path`,
+          credentialsConfigPath,
+          false, // globalConfig?
+          false, // add?
+          configPath
+        )
+
+        // Container submodule git directory
+        const githubWorkspace = process.env['GITHUB_WORKSPACE']
+        assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
+        let relativeSubmoduleGitDir = path.relative(
+          githubWorkspace,
+          submoduleGitDir
+        )
+        relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+        const containerSubmoduleGitDir = path.posix.join(
+          '/github/workspace',
+          relativeSubmoduleGitDir
+        )
+
+        // Configure container includeIf
+        await this.git.config(
+          `includeIf.gitdir:${containerSubmoduleGitDir}.path`,
+          containerCredentialsPath,
+          false, // globalConfig?
+          false, // add?
+          configPath
+        )
       }
 
       if (this.settings.sshKey) {
@@ -201,6 +243,10 @@ class GitAuthHelper {
     }
   }
 
+  /**
+   * Configures SSH authentication by writing the SSH key and known hosts,
+   * and setting up the GIT_SSH_COMMAND environment variable.
+   */
   private async configureSsh(): Promise<void> {
     if (!this.settings.sshKey) {
       return
@@ -272,57 +318,116 @@ class GitAuthHelper {
     }
   }
 
-  private async configureToken(
-    configPath?: string,
-    globalConfig?: boolean
-  ): Promise<void> {
-    // Validate args
-    assert.ok(
-      (configPath && globalConfig) || (!configPath && !globalConfig),
-      'Unexpected configureToken parameter combinations'
-    )
-
-    // Default config path
-    if (!configPath && !globalConfig) {
-      configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
-    }
-
-    // Configure a placeholder value. This approach avoids the credential being captured
-    // by process creation audit events, which are commonly logged. For more information,
-    // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
+  /**
+   * Configures token-based authentication by creating a credentials config file
+   * and setting up includeIf entries to reference it.
+   * @param globalConfig Whether to configure global config instead of local
+   */
+  private async configureToken(globalConfig?: boolean): Promise<void> {
+    // Get the credentials config file path in RUNNER_TEMP
+    const credentialsConfigPath = this.getCredentialsConfigPath()
+
+    // Write placeholder to the separate credentials config file using git config.
+    // This approach avoids the credential being captured by process creation audit events,
+    // which are commonly logged. For more information, refer to
+    // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
     await this.git.config(
       this.tokenConfigKey,
       this.tokenPlaceholderConfigValue,
-      globalConfig
+      false, // globalConfig?
+      false, // add?
+      credentialsConfigPath
     )
 
-    // Replace the placeholder
-    await this.replaceTokenPlaceholder(configPath || '')
-  }
-
-  private async replaceTokenPlaceholder(configPath: string): Promise<void> {
-    assert.ok(configPath, 'configPath is not defined')
-    let content = (await fs.promises.readFile,(configPath)).toString()
+    // Replace the placeholder in the credentials config file
+    let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
     const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
     if (
       placeholderIndex < 0 ||
       placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
     ) {
-      throw new Error(`Unable to replace auth placeholder in ${configPath}`)
+      throw new Error(
+        `Unable to replace auth placeholder in ${credentialsConfigPath}`
+      )
     }
     assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
     content = content.replace(
       this.tokenPlaceholderConfigValue,
       this.tokenConfigValue
     )
-    await fs.promises.writeFile(configPath, content)
+    await fs.promises.writeFile(credentialsConfigPath, content)
+
+    // Add include or includeIf to reference the credentials config
+    if (globalConfig) {
+      // Global config file is temporary
+      await this.git.config(
+        'include.path',
+        credentialsConfigPath,
+        true // globalConfig?
+      )
+    } else {
+      // Host git directory
+      let gitDir = path.join(this.git.getWorkingDirectory(), '.git')
+      gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+
+      // Configure host includeIf
+      const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
+      await this.git.config(hostIncludeKey, credentialsConfigPath)
+
+      // Container git directory
+      const workingDirectory = this.git.getWorkingDirectory()
+      const githubWorkspace = process.env['GITHUB_WORKSPACE']
+      assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
+      let relativePath = path.relative(githubWorkspace, workingDirectory)
+      relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows
+      const containerGitDir = path.posix.join(
+        '/github/workspace',
+        relativePath,
+        '.git'
+      )
+
+      // Container credentials config path
+      const containerCredentialsPath = path.posix.join(
+        '/github/runner_temp',
+        path.basename(credentialsConfigPath)
+      )
+
+      // Configure container includeIf
+      const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
+      await this.git.config(containerIncludeKey, containerCredentialsPath)
+    }
+  }
+
+  /**
+   * Gets or creates the path to the credentials config file in RUNNER_TEMP.
+   * @returns The absolute path to the credentials config file
+   */
+  private getCredentialsConfigPath(): string {
+    if (this.credentialsConfigPath) {
+      return this.credentialsConfigPath
+    }
+
+    const runnerTemp = process.env['RUNNER_TEMP'] || ''
+    assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
+
+    // Create a unique filename for this checkout instance
+    const configFileName = `git-credentials-${uuid()}.config`
+    this.credentialsConfigPath = path.join(runnerTemp, configFileName)
+
+    core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
+    return this.credentialsConfigPath
   }
 
+  /**
+   * Removes SSH authentication configuration by cleaning up SSH keys,
+   * known hosts files, and SSH command configurations.
+   */
   private async removeSsh(): Promise<void> {
     // SSH key
     const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
     if (keyPath) {
       try {
+        core.info(`Removing SSH key '${keyPath}'`)
         await io.rmRF(keyPath)
       } catch (err) {
         core.debug(`${(err as any)?.message ?? err}`)
@@ -335,40 +440,149 @@ class GitAuthHelper {
       this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
     if (knownHostsPath) {
       try {
+        core.info(`Removing SSH known hosts '${knownHostsPath}'`)
         await io.rmRF(knownHostsPath)
-      } catch {
-        // Intentionally empty
+      } catch (err) {
+        core.debug(`${(err as any)?.message ?? err}`)
+        core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
       }
     }
 
     // SSH command
+    core.info('Removing SSH command configuration')
     await this.removeGitConfig(SSH_COMMAND_KEY)
+    await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY)
   }
 
+  /**
+   * Removes token-based authentication by cleaning up HTTP headers,
+   * includeIf entries, and credentials config files.
+   */
   private async removeToken(): Promise<void> {
-    // HTTP extra header
+    // Remove HTTP extra header
+    core.info('Removing HTTP extra header')
     await this.removeGitConfig(this.tokenConfigKey)
-  }
+    await this.removeSubmoduleGitConfig(this.tokenConfigKey)
+
+    // Collect credentials config paths that need to be removed
+    const credentialsPaths = new Set<string>()
+
+    // Remove includeIf entries that point to git-credentials-*.config files
+    core.info('Removing includeIf entries pointing to credentials config files')
+    const mainCredentialsPaths = await this.removeIncludeIfCredentials()
+    mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
+
+    // Remove submodule includeIf entries that point to git-credentials-*.config files
+    const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true)
+    for (const configPath of submoduleConfigPaths) {
+      const submoduleCredentialsPaths =
+        await this.removeIncludeIfCredentials(configPath)
+      submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
+    }
 
-  private async removeGitConfig(
-    configKey: string,
-    submoduleOnly: boolean = false
-  ): Promise<void> {
-    if (!submoduleOnly) {
-      if (
-        (await this.git.configExists(configKey)) &&
-        !(await this.git.tryConfigUnset(configKey))
-      ) {
-        // Load the config contents
-        core.warning(`Failed to remove '${configKey}' from the git config`)
+    // Remove credentials config files
+    for (const credentialsPath of credentialsPaths) {
+      // Only remove credentials config files if they are under RUNNER_TEMP
+      const runnerTemp = process.env['RUNNER_TEMP']
+      assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
+      if (credentialsPath.startsWith(runnerTemp)) {
+        try {
+          core.info(`Removing credentials config '${credentialsPath}'`)
+          await io.rmRF(credentialsPath)
+        } catch (err) {
+          core.debug(`${(err as any)?.message ?? err}`)
+          core.warning(
+            `Failed to remove credentials config '${credentialsPath}'`
+          )
+        }
+      } else {
+        core.debug(
+          `Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`
+        )
       }
     }
+  }
+
+  /**
+   * Removes a git config key from the local repository config.
+   * @param configKey The git config key to remove
+   */
+  private async removeGitConfig(configKey: string): Promise<void> {
+    if (
+      (await this.git.configExists(configKey)) &&
+      !(await this.git.tryConfigUnset(configKey))
+    ) {
+      // Load the config contents
+      core.warning(`Failed to remove '${configKey}' from the git config`)
+    }
+  }
 
+  /**
+   * Removes a git config key from all submodule configs.
+   * @param configKey The git config key to remove
+   */
+  private async removeSubmoduleGitConfig(configKey: string): Promise<void> {
     const pattern = regexpHelper.escape(configKey)
     await this.git.submoduleForeach(
-      // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
+      // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
       `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
       true
     )
   }
+
+  /**
+   * Removes includeIf entries that point to git-credentials-*.config files.
+   * @param configPath Optional path to a specific git config file to operate on
+   * @returns Array of unique credentials config file paths that were found and removed
+   */
+  private async removeIncludeIfCredentials(
+    configPath?: string
+  ): Promise<string[]> {
+    const credentialsPaths = new Set<string>()
+
+    try {
+      // Get all includeIf.gitdir keys
+      const keys = await this.git.tryGetConfigKeys(
+        '^includeIf\\.gitdir:',
+        false, // globalConfig?
+        configPath
+      )
+
+      for (const key of keys) {
+        // Get all values for this key
+        const values = await this.git.tryGetConfigValues(
+          key,
+          false, // globalConfig?
+          configPath
+        )
+        if (values.length > 0) {
+          // Remove only values that match git-credentials-<uuid>.config pattern
+          for (const value of values) {
+            if (this.testCredentialsConfigPath(value)) {
+              credentialsPaths.add(value)
+              await this.git.tryConfigUnsetValue(key, value, false, configPath)
+            }
+          }
+        }
+      }
+    } catch (err) {
+      // Ignore errors - this is cleanup code
+      if (configPath) {
+        core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`)
+      } else {
+        core.debug(`Error during includeIf cleanup: ${err}`)
+      }
+    }
+
+    return Array.from(credentialsPaths)
+  }
+
+  /**
+   * Tests if a path matches the git-credentials-*.config pattern.
+   * @param path The path to test
+   * @returns True if the path matches the credentials config pattern
+   */
+  private testCredentialsConfigPath(path: string): boolean {
+    return /git-credentials-[0-9a-f-]+\.config$/i.test(path)
+  }
 }
diff --git src/git-command-manager.ts src/git-command-manager.ts
index 8e42a387f..a45e15a86 100644
--- src/git-command-manager.ts
+++ src/git-command-manager.ts
@@ -28,7 +28,8 @@ export interface IGitCommandManager {
     configKey: string,
     configValue: string,
     globalConfig?: boolean,
-    add?: boolean
+    add?: boolean,
+    configFile?: string
   ): Promise<void>
   configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
   fetch(
@@ -41,6 +42,7 @@ export interface IGitCommandManager {
     }
   ): Promise<void>
   getDefaultBranch(repositoryUrl: string): Promise<string>
+  getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
   getWorkingDirectory(): string
   init(): Promise<void>
   isDetached(): Promise<boolean>
@@ -59,8 +61,24 @@ export interface IGitCommandManager {
   tagExists(pattern: string): Promise<boolean>
   tryClean(): Promise<boolean>
   tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
+  tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean>
   tryDisableAutomaticGarbageCollection(): Promise<boolean>
   tryGetFetchUrl(): Promise<string>
+  tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
+  tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
   tryReset(): Promise<boolean>
   version(): Promise<GitVersion>
 }
@@ -223,9 +241,15 @@ class GitCommandManager {
     configKey: string,
     configValue: string,
     globalConfig?: boolean,
-    add?: boolean
+    add?: boolean,
+    configFile?: string
   ): Promise<void> {
-    const args: string[] = ['config', globalConfig ? '--global' : '--local']
+    const args: string[] = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
     if (add) {
       args.push('--add')
     }
@@ -323,6 +347,21 @@ class GitCommandManager {
     throw new Error('Unexpected output when retrieving default branch')
   }
 
+  async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
+    // Get submodule config file paths.
+    // Use `--show-origin` to get the config file path for each submodule.
+    const output = await this.submoduleForeach(
+      `git config --local --show-origin --name-only --get-regexp remote.origin.url`,
+      recursive
+    )
+
+    // Extract config file paths from the output (lines starting with "file:").
+    const configPaths =
+      output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+
+    return configPaths
+  }
+
   getWorkingDirectory(): string {
     return this.workingDirectory
   }
@@ -455,6 +494,24 @@ class GitCommandManager {
     return output.exitCode === 0
   }
 
+  async tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--unset', configKey, configValue)
+
+    const output = await this.execGit(args, true)
+    return output.exitCode === 0
+  }
+
   async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
     const output = await this.execGit(
       ['config', '--local', 'gc.auto', '0'],
@@ -481,6 +538,56 @@ class GitCommandManager {
     return stdout
   }
 
+  async tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--get-all', configKey)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(value => value.trim())
+  }
+
+  async tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--name-only', '--get-regexp', pattern)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(key => key.trim())
+  }
+
   async tryReset(): Promise<boolean> {
     const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
     return output.exitCode === 0
diff --git src/misc/generate-docs.ts src/misc/generate-docs.ts
index 4b3c8ff52..6d4816f15 100644
--- src/misc/generate-docs.ts
+++ src/misc/generate-docs.ts
@@ -120,7 +120,7 @@ function updateUsage(
 }
 
 updateUsage(
-  'actions/checkout@v4',
+  'actions/checkout@v5',
   path.join(__dirname, '..', '..', 'action.yml'),
   path.join(__dirname, '..', '..', 'README.md')
 )

Description

This PR upgrades the checkout action to v6, which includes several major changes:

  1. Node.js 24 Runtime Migration: Updates the action from Node.js 20 to Node.js 24, requiring a minimum Actions Runner version of v2.327.1.

  2. Credential Storage Refactoring: Changes how Git credentials are persisted. Instead of storing credentials directly in the local git config, credentials are now stored in a separate file under $RUNNER_TEMP (e.g., git-credentials-<uuid>.config), and git config uses includeIf.gitdir: entries to reference this file. This requires a minimum Actions Runner version of v2.329.0 for Docker container action scenarios.

  3. Test Infrastructure Updates: Updates test workflows to use Node.js 24 and adjusts test scenarios to properly handle the new checkout behavior with explicit path specifications.

Possible Issues

  1. Breaking Change for Self-Hosted Runners: Users with self-hosted runners below v2.327.1 will not be able to use v5, and those below v2.329.0 will experience issues with Docker container actions.

  2. Path Handling Complexity: The new credential storage mechanism needs to handle both host paths and container paths (e.g., /github/workspace vs actual host paths). This increases complexity and potential for path-related bugs.

  3. Submodule Configuration Changes: The submodule authentication logic has been significantly refactored. The old approach used placeholder values and replacement; the new approach uses includeIf entries. This could break edge cases.

  4. Test Coverage: The git-auth-helper.test.ts file has extensive mock modifications, but there's risk that the mocks don't accurately reflect real Git behavior, especially around the new includeIf functionality.

  5. Cleanup Reliability: The cleanup process now needs to find and remove multiple includeIf entries and separate credential files. If any step fails, credentials could be left behind.

Security Hotspots

  1. Credential File Permissions (High Risk): The new credentials config file is created in $RUNNER_TEMP but the code doesn't explicitly set restrictive file permissions. On Unix systems, this could allow other users to read the credentials if the umask is permissive.

    • Location: src/git-auth-helper.ts, getCredentialsConfigPath() method
    • Recommendation: Set file permissions to 0600 immediately after creation
  2. Credential Cleanup Validation (Medium Risk): The cleanup logic uses a regex pattern (/git-credentials-[0-9a-f-]+\.config$/i) to identify credential files to delete. While this is reasonably specific, there's a risk of:

    • Missing credential files if the pattern doesn't match
    • Accidentally deleting unrelated files if the pattern is too broad
    • Location: src/git-auth-helper.ts, removeIncludeIfCredentials() and testCredentialsConfigPath() methods
  3. Path Traversal in Container Paths (Low Risk): The code constructs container paths using path.posix.join('/github/runner_temp', path.basename(credentialsFilePath)). While path.basename() should strip directory traversal attempts, malicious environment variables or git config could potentially influence this.

    • Location: src/git-auth-helper.ts, multiple locations using containerCredentialsPath
  4. Token Exposure in Process Arguments (Mitigated, but verify): The PR maintains the placeholder replacement approach to avoid token exposure in process arguments. However, the token still passes through file I/O operations which could be logged or cached.

    • Location: src/git-auth-helper.ts, configureToken() method
  5. RUNNER_TEMP Validation (Low Risk): The code asserts that RUNNER_TEMP is defined but doesn't validate that it's a safe directory or check if it exists and is writable before attempting to create credential files.

    • Location: src/git-auth-helper.ts, getCredentialsConfigPath() method

Privacy Hotspots

  1. Git Credential Exposure in Logs (Medium Risk): While the code uses placeholder values to avoid credential exposure in process creation events, the debug statements include credential file paths which could inadvertently reveal credential locations in logs:

    core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
    • Location: src/git-auth-helper.ts, getCredentialsConfigPath() method
  2. Repository Path Disclosure: The new includeIf configuration includes full repository paths from the host filesystem, which could reveal information about the runner's directory structure in git config files.

    • Location: src/git-auth-helper.ts, configureToken() and configureSubmoduleAuth() methods
Changes

Changes

.github/workflows/check-dist.yml & .github/workflows/test.yml

  • Updated Node.js version from 20.x to 24.x
  • Added explicit path parameters to checkout actions in tests to avoid conflicts
  • Removed workaround for checkout post-cleanup

.github/workflows/update-main-version.yml

  • Added v5 to the list of major versions to update

CHANGELOG.md

  • Added entries for v6.0.0 (persist creds to separate file, Node.js 24 support)
  • Added entries for v5.0.1, v5.0.0, and v4.3.1 (backported cleanup fixes)

README.md

  • Updated all examples to use actions/checkout@v5
  • Added "What's new" sections for v6 and v5
  • Documented minimum runner requirements (v2.327.1 for v5, v2.329.0 for v6 Docker support)
  • Added table of contents

action.yml

  • Changed runtime from node20 to node24

package.json

  • Version bump from 4.3.0 to 5.0.0
  • Updated @types/node from ^20.12.12 to ^24.1.0

src/git-auth-helper.ts (Major refactoring)

  • New credential storage approach: Created getCredentialsConfigPath() to generate unique credential file paths in $RUNNER_TEMP
  • Refactored configureToken(): Now writes credentials to separate file and uses includeIf.gitdir: entries to reference it (supports both host and container paths)
  • Refactored configureSubmoduleAuth(): Replaced placeholder-based approach with includeIf entries pointing to shared credentials file
  • Enhanced cleanup in removeToken(): Now removes includeIf entries and deletes credential files
  • New helper methods:
    • removeIncludeIfCredentials(): Finds and removes includeIf entries pointing to credential files
    • testCredentialsConfigPath(): Validates credential file path patterns
    • removeSubmoduleGitConfig(): Extracted submodule config removal logic
  • Improved logging: Added info-level logging for cleanup operations

src/git-command-manager.ts

  • Added configFile parameter to config() method to support writing to specific config files
  • New methods:
    • getSubmoduleConfigPaths(): Retrieves config file paths for all submodules
    • tryConfigUnsetValue(): Removes specific config key-value pairs
    • tryGetConfigValues(): Retrieves all values for a config key
    • tryGetConfigKeys(): Retrieves all keys matching a pattern

__test__/git-auth-helper.test.ts

  • Updated test expectations to check for includeIf entries instead of direct credential config
  • Added tests for verifying credentials are in separate config file
  • Added test removeAuth_removesTokenFromSubmodules to verify submodule cleanup
  • Added test testCredentialsConfigPath_matchesCredentialsConfigPaths to verify path pattern matching
  • Enhanced mocks for new git command manager methods
  • Updated mock implementations to support configFile parameter

__test__/git-directory-helper.test.ts

  • Added mocks for new git command manager methods

__test__/verify-submodules-*.sh

  • Added --includes flag to git config commands to ensure includeIf entries are processed

dist/index.js

  • Compiled distribution reflecting all source changes
sequenceDiagram
    participant GHA as GitHub Action
    participant Auth as GitAuthHelper
    participant Git as GitCommandManager
    participant FS as FileSystem
    participant GitConfig as Git Config

    Note over GHA,GitConfig: Configure Auth Phase
    GHA->>Auth: configureAuth()
    Auth->>Auth: getCredentialsConfigPath()
    Auth->>FS: Generate unique path in RUNNER_TEMP
    FS-->>Auth: credentials file path
    
    Auth->>Git: config(tokenKey, placeholder, false, false, credentialsFile)
    Git->>FS: Write placeholder to credentials file
    
    Auth->>FS: Read credentials file
    Auth->>FS: Replace placeholder with actual token
    
    Auth->>Git: config(includeIf.gitdir:HOST_PATH.path, credentialsFile)
    Git->>GitConfig: Add host includeIf entry
    
    Auth->>Git: config(includeIf.gitdir:CONTAINER_PATH.path, containerCredPath)
    Git->>GitConfig: Add container includeIf entry
    
    Note over GHA,GitConfig: Submodule Auth Phase
    Auth->>Git: getSubmoduleConfigPaths()
    Git-->>Auth: List of submodule config paths
    
    loop For each submodule
        Auth->>Git: config(includeIf.gitdir:SUBMODULE_PATH.path, credentialsFile)
        Git->>GitConfig: Add submodule includeIf entry
    end
    
    Note over GHA,GitConfig: Cleanup Phase
    GHA->>Auth: removeAuth()
    
    Auth->>Auth: removeIncludeIfCredentials()
    Auth->>Git: tryGetConfigKeys(^includeIf\\.gitdir:)
    Git-->>Auth: List of includeIf keys
    
    loop For each includeIf key
        Auth->>Git: tryGetConfigValues(key)
        Git-->>Auth: List of credential file paths
        
        loop For each credential path
            Auth->>Git: tryConfigUnsetValue(key, value)
            Git->>GitConfig: Remove includeIf entry
        end
    end
    
    loop For each submodule
        Auth->>Auth: removeIncludeIfCredentials(submoduleConfigPath)
        Note right of Auth: Same process as above
    end
    
    Auth->>FS: Delete credentials file(s)
Loading

@renovate renovate bot force-pushed the renovate/actions-checkout-6-x branch from 7a756e8 to cc2799c Compare December 9, 2025 04:08
@github-actions
Copy link

github-actions bot commented Dec 9, 2025

[puLL-Merge] - actions/checkout@v4.3.1..v6.0.1

Diff
diff --git .github/workflows/check-dist.yml .github/workflows/check-dist.yml
index 53902eeb9..c7d49620f 100644
--- .github/workflows/check-dist.yml
+++ .github/workflows/check-dist.yml
@@ -22,12 +22,12 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v4.1.6
+      - uses: actions/checkout@v6
 
-      - name: Set Node.js 20.x
+      - name: Set Node.js 24.x
         uses: actions/setup-node@v4
         with:
-          node-version: 20.x
+          node-version: 24.x
 
       - name: Install dependencies
         run: npm ci
diff --git .github/workflows/codeql-analysis.yml .github/workflows/codeql-analysis.yml
index 778d474d8..377fae951 100644
--- .github/workflows/codeql-analysis.yml
+++ .github/workflows/codeql-analysis.yml
@@ -39,7 +39,7 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v4.1.6
+      uses: actions/checkout@v6
 
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v3
diff --git .github/workflows/licensed.yml .github/workflows/licensed.yml
index 1f71aa749..36e70e2c1 100644
--- .github/workflows/licensed.yml
+++ .github/workflows/licensed.yml
@@ -9,6 +9,6 @@ jobs:
     runs-on: ubuntu-latest
     name: Check licenses
     steps:
-      - uses: actions/checkout@v4.1.6
+      - uses: actions/checkout@v6
       - run: npm ci
       - run: npm run licensed-check
\ No newline at end of file
diff --git .github/workflows/publish-immutable-actions.yml .github/workflows/publish-immutable-actions.yml
index 87c020728..44d571ba9 100644
--- .github/workflows/publish-immutable-actions.yml
+++ .github/workflows/publish-immutable-actions.yml
@@ -14,7 +14,7 @@ jobs:
 
     steps:
       - name: Checking out
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
       - name: Publish
         id: publish
         uses: actions/publish-immutable-action@0.0.3
diff --git .github/workflows/test.yml .github/workflows/test.yml
index cde9f060e..fe2539f5a 100644
--- .github/workflows/test.yml
+++ .github/workflows/test.yml
@@ -18,8 +18,8 @@ jobs:
     steps:
       - uses: actions/setup-node@v4
         with:
-          node-version: 20.x
-      - uses: actions/checkout@v4.1.6
+          node-version: 24.x
+      - uses: actions/checkout@v6
       - run: npm ci
       - run: npm run build
       - run: npm run format-check
@@ -37,7 +37,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
 
       # Basic checkout
       - name: Checkout basic
@@ -165,6 +165,22 @@ jobs:
       - name: Verify submodules recursive
         run: __test__/verify-submodules-recursive.sh
 
+      # Worktree credentials
+      - name: Checkout for worktree test
+        uses: ./
+        with:
+          path: worktree-test
+      - name: Verify worktree credentials
+        shell: bash
+        run: __test__/verify-worktree.sh worktree-test worktree-branch
+
+      # Worktree credentials in container step
+      - name: Verify worktree credentials in container step
+        if: runner.os == 'Linux'
+        uses: docker://bitnami/git:latest
+        with:
+          args: bash __test__/verify-worktree.sh worktree-test container-worktree-branch
+
       # Basic checkout using REST API
       - name: Remove basic
         if: runner.os != 'windows'
@@ -202,7 +218,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
 
       # Basic checkout using git
       - name: Checkout basic
@@ -234,7 +250,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
 
       # Basic checkout using git
       - name: Checkout basic
@@ -264,7 +280,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
         with:
           path: localClone
 
@@ -291,8 +307,8 @@ jobs:
           git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
 
       # needed to make checkout post cleanup succeed
-      - name: Fix Checkout v4
-        uses: actions/checkout@v4.1.6
+      - name: Fix Checkout v6
+        uses: actions/checkout@v6
         with:
           path: localClone
 
@@ -301,13 +317,16 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
+        with:
+          path: actions-checkout
 
       # Basic checkout using git
       - name: Checkout basic
         id: checkout
-        uses: ./
+        uses: ./actions-checkout
         with:
+          path: cloned-using-local-action
           ref: test-data/v2/basic
 
       # Verify output
@@ -325,7 +344,3 @@ jobs:
             echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
             exit 1
           fi
-
-      # needed to make checkout post cleanup succeed
-      - name: Fix Checkout
-        uses: actions/checkout@v4.1.6
diff --git .github/workflows/update-main-version.yml .github/workflows/update-main-version.yml
index 7bec7d5a8..b3b23fe4e 100644
--- .github/workflows/update-main-version.yml
+++ .github/workflows/update-main-version.yml
@@ -11,6 +11,7 @@ on:
         type: choice
         description: The major version to update
         options:
+          - v5
           - v4
           - v3
           - v2
@@ -22,7 +23,7 @@ jobs:
     # Note this update workflow can also be used as a rollback tool.
     # For that reason, it's best to pin `actions/checkout` to a known, stable version
     # (typically, about two releases back).
-    - uses: actions/checkout@v4.1.6
+    - uses: actions/checkout@v6
       with:
         fetch-depth: 0
     - name: Git config
diff --git .github/workflows/update-test-ubuntu-git.yml .github/workflows/update-test-ubuntu-git.yml
index 5c252b98d..10e4dac93 100644
--- .github/workflows/update-test-ubuntu-git.yml
+++ .github/workflows/update-test-ubuntu-git.yml
@@ -26,7 +26,7 @@ jobs:
  
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
 
       # Use `docker/login-action` to log in to GHCR.io. 
       # Once published, the packages are scoped to the account defined here.
diff --git CHANGELOG.md CHANGELOG.md
index baf5c2d7e..6d5a6f302 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,6 +1,19 @@
 # Changelog
 
-## V4.3.0
+## v6.0.0
+* Persist creds to a separate file by @ericsciple in https://github.com/actions/checkout/pull/2286
+* Update README to include Node.js 24 support details and requirements by @salmanmkc in https://github.com/actions/checkout/pull/2248
+
+## v5.0.1
+* Port v6 cleanup to v5 by @ericsciple in https://github.com/actions/checkout/pull/2301
+
+## v5.0.0
+* Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
+
+## v4.3.1
+* Port v6 cleanup to v4 by @ericsciple in https://github.com/actions/checkout/pull/2305
+
+## v4.3.0
 * docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
 * Add internal repos for checking out multiple repositories by @mouismail in https://github.com/actions/checkout/pull/1977
 * Documentation update - add recommended permissions to Readme by @benwells in https://github.com/actions/checkout/pull/2043
diff --git README.md README.md
index 8969446c3..f0f65f9f6 100644
--- README.md
+++ README.md
@@ -1,6 +1,22 @@
 [![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml)
 
-# Checkout V4
+# Checkout v6
+
+## What's new
+
+- Improved credential security: `persist-credentials` now stores credentials in a separate file under `$RUNNER_TEMP` instead of directly in `.git/config`
+- No workflow changes required — `git fetch`, `git push`, etc. continue to work automatically
+- Running authenticated git commands from a [Docker container action](https://docs.github.com/actions/sharing-automations/creating-actions/creating-a-docker-container-action) requires Actions Runner [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) or later
+
+# Checkout v5
+
+## What's new
+
+- Updated to the node24 runtime
+  - This requires a minimum Actions Runner version of [v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) to run.
+
+
+# Checkout v4
 
 This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
 
@@ -36,7 +52,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 <!-- start usage -->
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     # Repository name with owner. For example, actions/checkout
     # Default: ${{ github.repository }}
@@ -149,24 +165,33 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 # Scenarios
 
-- [Fetch only the root files](#Fetch-only-the-root-files)
-- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
-- [Fetch only a single file](#Fetch-only-a-single-file)
-- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
-- [Checkout a different branch](#Checkout-a-different-branch)
-- [Checkout HEAD^](#Checkout-HEAD)
-- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
-- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested)
-- [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
-- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
-- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
-- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
-- [Push a commit to a PR using the built-in token](#Push-a-commit-to-a-PR-using-the-built-in-token)
+- [Checkout V5](#checkout-v5)
+  - [What's new](#whats-new)
+- [Checkout V4](#checkout-v4)
+    - [Note](#note)
+- [What's new](#whats-new-1)
+- [Usage](#usage)
+- [Scenarios](#scenarios)
+  - [Fetch only the root files](#fetch-only-the-root-files)
+  - [Fetch only the root files and `.github` and `src` folder](#fetch-only-the-root-files-and-github-and-src-folder)
+  - [Fetch only a single file](#fetch-only-a-single-file)
+  - [Fetch all history for all tags and branches](#fetch-all-history-for-all-tags-and-branches)
+  - [Checkout a different branch](#checkout-a-different-branch)
+  - [Checkout HEAD^](#checkout-head)
+  - [Checkout multiple repos (side by side)](#checkout-multiple-repos-side-by-side)
+  - [Checkout multiple repos (nested)](#checkout-multiple-repos-nested)
+  - [Checkout multiple repos (private)](#checkout-multiple-repos-private)
+  - [Checkout pull request HEAD commit instead of merge commit](#checkout-pull-request-head-commit-instead-of-merge-commit)
+  - [Checkout pull request on closed event](#checkout-pull-request-on-closed-event)
+  - [Push a commit using the built-in token](#push-a-commit-using-the-built-in-token)
+  - [Push a commit to a PR using the built-in token](#push-a-commit-to-a-pr-using-the-built-in-token)
+- [Recommended permissions](#recommended-permissions)
+- [License](#license)
 
 ## Fetch only the root files
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     sparse-checkout: .
 ```
@@ -174,7 +199,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch only the root files and `.github` and `src` folder
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     sparse-checkout: |
       .github
@@ -184,7 +209,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch only a single file
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     sparse-checkout: |
       README.md
@@ -194,7 +219,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch all history for all tags and branches
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     fetch-depth: 0
 ```
@@ -202,7 +227,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout a different branch
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     ref: my-branch
 ```
@@ -210,7 +235,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout HEAD^
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     fetch-depth: 2
 - run: git checkout HEAD^
@@ -220,12 +245,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
   with:
     path: main
 
 - name: Checkout tools repo
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
   with:
     repository: my-org/my-tools
     path: my-tools
@@ -236,10 +261,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
 
 - name: Checkout tools repo
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
   with:
     repository: my-org/my-tools
     path: my-tools
@@ -250,12 +275,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
   with:
     path: main
 
 - name: Checkout private tools
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
   with:
     repository: my-org/my-private-tools
     token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
@@ -268,7 +293,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout pull request HEAD commit instead of merge commit
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     ref: ${{ github.event.pull_request.head.sha }}
 ```
@@ -284,7 +309,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v6
 ```
 
 ## Push a commit using the built-in token
@@ -295,7 +320,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v6
       - run: |
           date > generated.txt
           # Note: the following account information will not work on GHES
@@ -317,7 +342,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v6
         with:
           ref: ${{ github.head_ref }}
       - run: |
diff --git __test__/git-auth-helper.test.ts __test__/git-auth-helper.test.ts
index 7633704cc..ad3566ad6 100644
--- __test__/git-auth-helper.test.ts
+++ __test__/git-auth-helper.test.ts
@@ -86,16 +86,29 @@ describe('git-auth-helper tests', () => {
     // Act
     await authHelper.configureAuth()
 
-    // Assert config
-    const configContent = (
+    // Assert config - check that .git/config contains includeIf entries
+    const localConfigContent = (
       await fs.promises.readFile(localGitConfigPath)
     ).toString()
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:')
+    ).toBeGreaterThanOrEqual(0)
+
+    // Assert credentials config file contains the actual credentials
+    const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(1)
+    const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+    const credentialsContent = (
+      await fs.promises.readFile(credentialsConfigPath)
+    ).toString()
     const basicCredential = Buffer.from(
       `x-access-token:${settings.authToken}`,
       'utf8'
     ).toString('base64')
     expect(
-      configContent.indexOf(
+      credentialsContent.indexOf(
         `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
       )
     ).toBeGreaterThanOrEqual(0)
@@ -120,7 +133,7 @@ describe('git-auth-helper tests', () => {
     'inject https://github.com as github server url'
   it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
     await testAuthHeader(
-      configureAuth_AcceptsGitHubServerUrl,
+      configureAuth_AcceptsGitHubServerUrlSetToGHEC,
       'https://github.com'
     )
   })
@@ -141,12 +154,17 @@ describe('git-auth-helper tests', () => {
       // Act
       await authHelper.configureAuth()
 
-      // Assert config
-      const configContent = (
-        await fs.promises.readFile(localGitConfigPath)
+      // Assert config - check credentials config file (not local .git/config)
+      const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+        f => f.startsWith('git-credentials-') && f.endsWith('.config')
+      )
+      expect(credentialsFiles.length).toBe(1)
+      const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+      const credentialsContent = (
+        await fs.promises.readFile(credentialsConfigPath)
       ).toString()
       expect(
-        configContent.indexOf(
+        credentialsContent.indexOf(
           `http.https://github.com/.extraheader AUTHORIZATION`
         )
       ).toBeGreaterThanOrEqual(0)
@@ -251,13 +269,16 @@ describe('git-auth-helper tests', () => {
       expectedSshCommand
     )
 
-    // Asserty git config
+    // Assert git config
     const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
       .toString()
       .split('\n')
       .filter(x => x)
-    expect(gitConfigLines).toHaveLength(1)
-    expect(gitConfigLines[0]).toMatch(/^http\./)
+    // Should have includeIf entries pointing to credentials file
+    expect(gitConfigLines.length).toBeGreaterThan(0)
+    expect(
+      gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
+    ).toBeTruthy()
   })
 
   const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
@@ -419,8 +440,20 @@ describe('git-auth-helper tests', () => {
     expect(
       configContent.indexOf('value-from-global-config')
     ).toBeGreaterThanOrEqual(0)
+    // Global config should have include.path pointing to credentials file
+    expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
+
+    // Check credentials in the separate config file
+    const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBeGreaterThan(0)
+    const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+    const credentialsContent = (
+      await fs.promises.readFile(credentialsConfigPath)
+    ).toString()
     expect(
-      configContent.indexOf(
+      credentialsContent.indexOf(
         `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
       )
     ).toBeGreaterThanOrEqual(0)
@@ -463,8 +496,20 @@ describe('git-auth-helper tests', () => {
       const configContent = (
         await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
       ).toString()
+      // Global config should have include.path pointing to credentials file
+      expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
+
+      // Check credentials in the separate config file
+      const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+        f => f.startsWith('git-credentials-') && f.endsWith('.config')
+      )
+      expect(credentialsFiles.length).toBeGreaterThan(0)
+      const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+      const credentialsContent = (
+        await fs.promises.readFile(credentialsConfigPath)
+      ).toString()
       expect(
-        configContent.indexOf(
+        credentialsContent.indexOf(
           `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
         )
       ).toBeGreaterThanOrEqual(0)
@@ -550,15 +595,15 @@ describe('git-auth-helper tests', () => {
       await authHelper.configureSubmoduleAuth()
 
       // Assert
-      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
+      // Should configure insteadOf (2 calls for two values)
+      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
         /unset-all.*insteadOf/
       )
-      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
-      expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
+      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
         /url.*insteadOf.*git@github.com:/
       )
-      expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
+      expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
         /url.*insteadOf.*org-123456@github.com:/
       )
     }
@@ -589,12 +634,12 @@ describe('git-auth-helper tests', () => {
       await authHelper.configureSubmoduleAuth()
 
       // Assert
-      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
+      // Should configure sshCommand (1 call)
+      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
         /unset-all.*insteadOf/
       )
-      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
-      expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
+      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
     }
   )
 
@@ -660,19 +705,201 @@ describe('git-auth-helper tests', () => {
     await setup(removeAuth_removesToken)
     const authHelper = gitAuthHelper.createAuthHelper(git, settings)
     await authHelper.configureAuth()
-    let gitConfigContent = (
+
+    // Verify includeIf entries exist in local config
+    let localConfigContent = (
       await fs.promises.readFile(localGitConfigPath)
     ).toString()
-    expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:')
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify both host and container includeIf entries are present
+    const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
+    expect(
+      localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify credentials file exists
+    let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(1)
+    const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
+
+    // Verify credentials file contains the auth token
+    let credentialsContent = (
+      await fs.promises.readFile(credentialsFilePath)
+    ).toString()
+    const basicCredential = Buffer.from(
+      `x-access-token:${settings.authToken}`,
+      'utf8'
+    ).toString('base64')
+    expect(
+      credentialsContent.indexOf(
+        `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
+      )
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify the includeIf entries point to the credentials file
+    const containerCredentialsPath = path.posix.join(
+      '/github/runner_temp',
+      path.basename(credentialsFilePath)
+    )
+    expect(
+      localConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      localConfigContent.indexOf(containerCredentialsPath)
+    ).toBeGreaterThanOrEqual(0)
 
     // Act
     await authHelper.removeAuth()
 
-    // Assert git config
-    gitConfigContent = (
+    // Assert all includeIf entries removed from local git config
+    localConfigContent = (
       await fs.promises.readFile(localGitConfigPath)
     ).toString()
-    expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
+    expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+    expect(
+      localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
+    ).toBeLessThan(0)
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
+    ).toBeLessThan(0)
+    expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+    expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+    // Assert credentials config file deleted
+    credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(0)
+
+    // Verify credentials file no longer exists on disk
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
+  })
+
+  const removeAuth_removesTokenFromSubmodules =
+    'removeAuth removes token from submodules'
+  it(removeAuth_removesTokenFromSubmodules, async () => {
+    // Arrange
+    await setup(removeAuth_removesTokenFromSubmodules)
+
+    // Create fake submodule config paths
+    const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
+    const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2')
+    const submodule1ConfigPath = path.join(submodule1Dir, 'config')
+    const submodule2ConfigPath = path.join(submodule2Dir, 'config')
+
+    await fs.promises.mkdir(submodule1Dir, {recursive: true})
+    await fs.promises.mkdir(submodule2Dir, {recursive: true})
+    await fs.promises.writeFile(submodule1ConfigPath, '')
+    await fs.promises.writeFile(submodule2ConfigPath, '')
+
+    // Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
+    const mockGetSubmoduleConfigPaths =
+      git.getSubmoduleConfigPaths as jest.Mock<any, any>
+    mockGetSubmoduleConfigPaths.mockResolvedValue([
+      submodule1ConfigPath,
+      submodule2ConfigPath
+    ])
+
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+    await authHelper.configureSubmoduleAuth()
+
+    // Verify credentials file exists
+    let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(1)
+    const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
+
+    // Verify submodule 1 config has includeIf entries
+    let submodule1Content = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
+    expect(
+      submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      submodule1Content.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify submodule 2 config has includeIf entries
+    let submodule2Content = (
+      await fs.promises.readFile(submodule2ConfigPath)
+    ).toString()
+    const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
+    expect(
+      submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      submodule2Content.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify both host and container paths are in each submodule config
+    const containerCredentialsPath = path.posix.join(
+      '/github/runner_temp',
+      path.basename(credentialsFilePath)
+    )
+    expect(
+      submodule1Content.indexOf(containerCredentialsPath)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      submodule2Content.indexOf(containerCredentialsPath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Act - ensure mock persists for removeAuth
+    mockGetSubmoduleConfigPaths.mockResolvedValue([
+      submodule1ConfigPath,
+      submodule2ConfigPath
+    ])
+    await authHelper.removeAuth()
+
+    // Assert submodule 1 includeIf entries removed
+    submodule1Content = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+    expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
+    expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+    // Assert submodule 2 includeIf entries removed
+    submodule2Content = (
+      await fs.promises.readFile(submodule2ConfigPath)
+    ).toString()
+    expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+    expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0)
+    expect(submodule2Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+    // Assert credentials config file deleted
+    credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(0)
+
+    // Verify credentials file no longer exists on disk
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
   })
 
   const removeGlobalConfig_removesOverride =
@@ -701,6 +928,52 @@ describe('git-auth-helper tests', () => {
       }
     }
   })
+
+  const testCredentialsConfigPath_matchesCredentialsConfigPaths =
+    'testCredentialsConfigPath matches credentials config paths'
+  it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => {
+    // Arrange
+    await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths)
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+
+    // Get a real credentials config path
+    const credentialsConfigPath = await (
+      authHelper as any
+    ).getCredentialsConfigPath()
+
+    // Act & Assert
+    expect(
+      (authHelper as any).testCredentialsConfigPath(credentialsConfigPath)
+    ).toBe(true)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config'
+      )
+    ).toBe(true)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
+      )
+    ).toBe(true)
+
+    // Test invalid paths
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/other-config.config'
+      )
+    ).toBe(false)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-invalid.config'
+      )
+    ).toBe(false)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-.config'
+      )
+    ).toBe(false)
+    expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
+  })
 })
 
 async function setup(testName: string): Promise<void> {
@@ -715,6 +988,7 @@ async function setup(testName: string): Promise<void> {
   await fs.promises.mkdir(tempHomedir, {recursive: true})
   process.env['RUNNER_TEMP'] = runnerTemp
   process.env['HOME'] = tempHomedir
+  process.env['GITHUB_WORKSPACE'] = workspace
 
   // Create git config
   globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
@@ -733,10 +1007,20 @@ async function setup(testName: string): Promise<void> {
     checkout: jest.fn(),
     checkoutDetach: jest.fn(),
     config: jest.fn(
-      async (key: string, value: string, globalConfig?: boolean) => {
-        const configPath = globalConfig
-          ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
-          : localGitConfigPath
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        add?: boolean,
+        configFile?: string
+      ) => {
+        const configPath =
+          configFile ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        // Ensure directory exists
+        await fs.promises.mkdir(path.dirname(configPath), {recursive: true})
         await fs.promises.appendFile(configPath, `\n${key} ${value}`)
       }
     ),
@@ -756,6 +1040,7 @@ async function setup(testName: string): Promise<void> {
     env: {},
     fetch: jest.fn(),
     getDefaultBranch: jest.fn(),
+    getSubmoduleConfigPaths: jest.fn(async () => []),
     getWorkingDirectory: jest.fn(() => workspace),
     init: jest.fn(),
     isDetached: jest.fn(),
@@ -794,8 +1079,72 @@ async function setup(testName: string): Promise<void> {
         return true
       }
     ),
+    tryConfigUnsetValue: jest.fn(
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ): Promise<boolean> => {
+        const targetConfigPath =
+          configPath ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        let content = await fs.promises.readFile(targetConfigPath)
+        let lines = content
+          .toString()
+          .split('\n')
+          .filter(x => x)
+          .filter(x => !(x.startsWith(key) && x.includes(value)))
+        await fs.promises.writeFile(targetConfigPath, lines.join('\n'))
+        return true
+      }
+    ),
     tryDisableAutomaticGarbageCollection: jest.fn(),
     tryGetFetchUrl: jest.fn(),
+    tryGetConfigValues: jest.fn(
+      async (
+        key: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ): Promise<string[]> => {
+        const targetConfigPath =
+          configPath ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        const content = await fs.promises.readFile(targetConfigPath)
+        const lines = content
+          .toString()
+          .split('\n')
+          .filter(x => x && x.startsWith(key))
+          .map(x => x.substring(key.length).trim())
+        return lines
+      }
+    ),
+    tryGetConfigKeys: jest.fn(
+      async (
+        pattern: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ): Promise<string[]> => {
+        const targetConfigPath =
+          configPath ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        const content = await fs.promises.readFile(targetConfigPath)
+        const lines = content
+          .toString()
+          .split('\n')
+          .filter(x => x)
+        const keys = lines
+          .filter(x => new RegExp(pattern).test(x.split(' ')[0]))
+          .map(x => x.split(' ')[0])
+        return [...new Set(keys)] // Remove duplicates
+      }
+    ),
     tryReset: jest.fn(),
     version: jest.fn()
   }
@@ -830,6 +1179,7 @@ async function setup(testName: string): Promise<void> {
 
 async function getActualSshKeyPath(): Promise<string> {
   let actualTempFiles = (await fs.promises.readdir(runnerTemp))
+    .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
     .sort()
     .map(x => path.join(runnerTemp, x))
   if (actualTempFiles.length === 0) {
@@ -843,6 +1193,7 @@ async function getActualSshKeyPath(): Promise<string> {
 
 async function getActualSshKnownHostsPath(): Promise<string> {
   let actualTempFiles = (await fs.promises.readdir(runnerTemp))
+    .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
     .sort()
     .map(x => path.join(runnerTemp, x))
   if (actualTempFiles.length === 0) {
diff --git __test__/git-directory-helper.test.ts __test__/git-directory-helper.test.ts
index 22e9ae6d4..de79dc890 100644
--- __test__/git-directory-helper.test.ts
+++ __test__/git-directory-helper.test.ts
@@ -471,6 +471,7 @@ async function setup(testName: string): Promise<void> {
     configExists: jest.fn(),
     fetch: jest.fn(),
     getDefaultBranch: jest.fn(),
+    getSubmoduleConfigPaths: jest.fn(async () => []),
     getWorkingDirectory: jest.fn(() => repositoryPath),
     init: jest.fn(),
     isDetached: jest.fn(),
@@ -493,12 +494,15 @@ async function setup(testName: string): Promise<void> {
       return true
     }),
     tryConfigUnset: jest.fn(),
+    tryConfigUnsetValue: jest.fn(),
     tryDisableAutomaticGarbageCollection: jest.fn(),
     tryGetFetchUrl: jest.fn(async () => {
       // Sanity check - this function shouldn't be called when the .git directory doesn't exist
       await fs.promises.stat(path.join(repositoryPath, '.git'))
       return repositoryUrl
     }),
+    tryGetConfigValues: jest.fn(),
+    tryGetConfigKeys: jest.fn(),
     tryReset: jest.fn(async () => {
       return true
     }),
diff --git __test__/verify-submodules-recursive.sh __test__/verify-submodules-recursive.sh
index 1b68f9b97..5ecbb42d0 100755
--- __test__/verify-submodules-recursive.sh
+++ __test__/verify-submodules-recursive.sh
@@ -17,7 +17,7 @@ fi
 
 echo "Testing persisted credential"
 pushd ./submodules-recursive/submodule-level-1/submodule-level-2
-git config --local --name-only --get-regexp http.+extraheader && git fetch
+git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
 if [ "$?" != "0" ]; then
     echo "Failed to validate persisted credential"
     popd
diff --git __test__/verify-submodules-true.sh __test__/verify-submodules-true.sh
index 43769fe06..4c311f846 100755
--- __test__/verify-submodules-true.sh
+++ __test__/verify-submodules-true.sh
@@ -17,7 +17,7 @@ fi
 
 echo "Testing persisted credential"
 pushd ./submodules-true/submodule-level-1
-git config --local --name-only --get-regexp http.+extraheader && git fetch
+git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
 if [ "$?" != "0" ]; then
     echo "Failed to validate persisted credential"
     popd
diff --git a/__test__/verify-worktree.sh b/__test__/verify-worktree.sh
new file mode 100755
index 000000000..3a4d3e4df
--- /dev/null
+++ __test__/verify-worktree.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+set -e
+
+# Verify worktree credentials
+# This test verifies that git credentials work in worktrees created after checkout
+# Usage: verify-worktree.sh <checkout-path> <worktree-name>
+
+CHECKOUT_PATH="$1"
+WORKTREE_NAME="$2"
+
+if [ -z "$CHECKOUT_PATH" ] || [ -z "$WORKTREE_NAME" ]; then
+  echo "Usage: verify-worktree.sh <checkout-path> <worktree-name>"
+  exit 1
+fi
+
+cd "$CHECKOUT_PATH"
+
+# Add safe directory for container environments
+git config --global --add safe.directory "*" 2>/dev/null || true
+
+# Show the includeIf configuration
+echo "Git config includeIf entries:"
+git config --list --show-origin | grep -i include || true
+
+# Create the worktree
+echo "Creating worktree..."
+git worktree add "../$WORKTREE_NAME" HEAD --detach
+
+# Change to worktree directory
+cd "../$WORKTREE_NAME"
+
+# Verify we're in a worktree
+echo "Verifying worktree gitdir:"
+cat .git
+
+# Verify credentials are available in worktree by checking extraheader is configured
+echo "Checking credentials in worktree..."
+if git config --list --show-origin | grep -q "extraheader"; then
+  echo "Credentials are configured in worktree"
+else
+  echo "ERROR: Credentials are NOT configured in worktree"
+  echo "Full git config:"
+  git config --list --show-origin
+  exit 1
+fi
+
+# Verify fetch works in the worktree
+echo "Fetching in worktree..."
+git fetch origin
+
+echo "Worktree credentials test passed!"
diff --git action.yml action.yml
index 6842eb843..767c41649 100644
--- action.yml
+++ action.yml
@@ -104,6 +104,6 @@ outputs:
   commit:
     description: 'The commit SHA that was checked out'
 runs:
-  using: node20
+  using: node24
   main: dist/index.js
   post: dist/index.js
diff --git dist/index.js dist/index.js
index f3ae6f3ea..b9b34d342 100644
--- dist/index.js
+++ dist/index.js
@@ -162,6 +162,7 @@ class GitAuthHelper {
         this.sshKeyPath = '';
         this.sshKnownHostsPath = '';
         this.temporaryHomePath = '';
+        this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP
         this.git = gitCommandManager;
         this.settings = gitSourceSettings || {};
         // Token auth header
@@ -229,15 +230,17 @@ class GitAuthHelper {
     configureGlobalAuth() {
         return __awaiter(this, void 0, void 0, function* () {
             // 'configureTempGlobalConfig' noops if already set, just returns the path
-            const newGitConfigPath = yield this.configureTempGlobalConfig();
+            yield this.configureTempGlobalConfig();
             try {
                 // Configure the token
-                yield this.configureToken(newGitConfigPath, true);
+                yield this.configureToken(true);
                 // Configure HTTPS instead of SSH
                 yield this.git.tryConfigUnset(this.insteadOfKey, true);
                 if (!this.settings.sshKey) {
                     for (const insteadOfValue of this.insteadOfValues) {
-                        yield this.git.config(this.insteadOfKey, insteadOfValue, true, true);
+                        yield this.git.config(this.insteadOfKey, insteadOfValue, true, // globalConfig?
+                        true // add?
+                        );
                     }
                 }
             }
@@ -252,19 +255,34 @@ class GitAuthHelper {
     configureSubmoduleAuth() {
         return __awaiter(this, void 0, void 0, function* () {
             // Remove possible previous HTTPS instead of SSH
-            yield this.removeGitConfig(this.insteadOfKey, true);
+            yield this.removeSubmoduleGitConfig(this.insteadOfKey);
             if (this.settings.persistCredentials) {
-                // Configure a placeholder value. This approach avoids the credential being captured
-                // by process creation audit events, which are commonly logged. For more information,
-                // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
-                const output = yield this.git.submoduleForeach(
-                // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
-                `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules);
-                // Replace the placeholder
-                const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+                // Get the credentials config file path in RUNNER_TEMP
+                const credentialsConfigPath = this.getCredentialsConfigPath();
+                // Container credentials config path
+                const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
+                // Get submodule config file paths.
+                const configPaths = yield this.git.getSubmoduleConfigPaths(this.settings.nestedSubmodules);
+                // For each submodule, configure includeIf entries pointing to the shared credentials file.
+                // Configure both host and container paths to support Docker container actions.
                 for (const configPath of configPaths) {
-                    core.debug(`Replacing token placeholder in '${configPath}'`);
-                    yield this.replaceTokenPlaceholder(configPath);
+                    // Submodule Git directory
+                    let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
+                    submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                    // Configure host includeIf
+                    yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
+                    false, // add?
+                    configPath);
+                    // Container submodule git directory
+                    const githubWorkspace = process.env['GITHUB_WORKSPACE'];
+                    assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
+                    let relativeSubmoduleGitDir = path.relative(githubWorkspace, submoduleGitDir);
+                    relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                    const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir);
+                    // Configure container includeIf
+                    yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
+                    false, // add?
+                    configPath);
                 }
                 if (this.settings.sshKey) {
                     // Configure core.sshCommand
@@ -295,6 +313,10 @@ class GitAuthHelper {
             }
         });
     }
+    /**
+     * Configures SSH authentication by writing the SSH key and known hosts,
+     * and setting up the GIT_SSH_COMMAND environment variable.
+     */
     configureSsh() {
         return __awaiter(this, void 0, void 0, function* () {
             if (!this.settings.sshKey) {
@@ -351,43 +373,94 @@ class GitAuthHelper {
             }
         });
     }
-    configureToken(configPath, globalConfig) {
-        return __awaiter(this, void 0, void 0, function* () {
-            // Validate args
-            assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations');
-            // Default config path
-            if (!configPath && !globalConfig) {
-                configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config');
-            }
-            // Configure a placeholder value. This approach avoids the credential being captured
-            // by process creation audit events, which are commonly logged. For more information,
-            // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
-            yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig);
-            // Replace the placeholder
-            yield this.replaceTokenPlaceholder(configPath || '');
-        });
-    }
-    replaceTokenPlaceholder(configPath) {
+    /**
+     * Configures token-based authentication by creating a credentials config file
+     * and setting up includeIf entries to reference it.
+     * @param globalConfig Whether to configure global config instead of local
+     */
+    configureToken(globalConfig) {
         return __awaiter(this, void 0, void 0, function* () {
-            assert.ok(configPath, 'configPath is not defined');
-            let content = (yield fs.promises.readFile(configPath)).toString();
+            // Get the credentials config file path in RUNNER_TEMP
+            const credentialsConfigPath = this.getCredentialsConfigPath();
+            // Write placeholder to the separate credentials config file using git config.
+            // This approach avoids the credential being captured by process creation audit events,
+            // which are commonly logged. For more information, refer to
+            // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
+            yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, false, // globalConfig?
+            false, // add?
+            credentialsConfigPath);
+            // Replace the placeholder in the credentials config file
+            let content = (yield fs.promises.readFile(credentialsConfigPath)).toString();
             const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
             if (placeholderIndex < 0 ||
                 placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) {
-                throw new Error(`Unable to replace auth placeholder in ${configPath}`);
+                throw new Error(`Unable to replace auth placeholder in ${credentialsConfigPath}`);
             }
             assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
             content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue);
-            yield fs.promises.writeFile(configPath, content);
+            yield fs.promises.writeFile(credentialsConfigPath, content);
+            // Add include or includeIf to reference the credentials config
+            if (globalConfig) {
+                // Global config file is temporary
+                yield this.git.config('include.path', credentialsConfigPath, true // globalConfig?
+                );
+            }
+            else {
+                // Host git directory
+                let gitDir = path.join(this.git.getWorkingDirectory(), '.git');
+                gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                // Configure host includeIf
+                const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
+                yield this.git.config(hostIncludeKey, credentialsConfigPath);
+                // Configure host includeIf for worktrees
+                const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`;
+                yield this.git.config(hostWorktreeIncludeKey, credentialsConfigPath);
+                // Container git directory
+                const workingDirectory = this.git.getWorkingDirectory();
+                const githubWorkspace = process.env['GITHUB_WORKSPACE'];
+                assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
+                let relativePath = path.relative(githubWorkspace, workingDirectory);
+                relativePath = relativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git');
+                // Container credentials config path
+                const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
+                // Configure container includeIf
+                const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
+                yield this.git.config(containerIncludeKey, containerCredentialsPath);
+                // Configure container includeIf for worktrees
+                const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`;
+                yield this.git.config(containerWorktreeIncludeKey, containerCredentialsPath);
+            }
         });
     }
+    /**
+     * Gets or creates the path to the credentials config file in RUNNER_TEMP.
+     * @returns The absolute path to the credentials config file
+     */
+    getCredentialsConfigPath() {
+        if (this.credentialsConfigPath) {
+            return this.credentialsConfigPath;
+        }
+        const runnerTemp = process.env['RUNNER_TEMP'] || '';
+        assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
+        // Create a unique filename for this checkout instance
+        const configFileName = `git-credentials-${(0, uuid_1.v4)()}.config`;
+        this.credentialsConfigPath = path.join(runnerTemp, configFileName);
+        core.debug(`Credentials config path: ${this.credentialsConfigPath}`);
+        return this.credentialsConfigPath;
+    }
+    /**
+     * Removes SSH authentication configuration by cleaning up SSH keys,
+     * known hosts files, and SSH command configurations.
+     */
     removeSsh() {
         return __awaiter(this, void 0, void 0, function* () {
-            var _a;
+            var _a, _b;
             // SSH key
             const keyPath = this.sshKeyPath || stateHelper.SshKeyPath;
             if (keyPath) {
                 try {
+                    core.info(`Removing SSH key '${keyPath}'`);
                     yield io.rmRF(keyPath);
                 }
                 catch (err) {
@@ -399,37 +472,136 @@ class GitAuthHelper {
             const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath;
             if (knownHostsPath) {
                 try {
+                    core.info(`Removing SSH known hosts '${knownHostsPath}'`);
                     yield io.rmRF(knownHostsPath);
                 }
-                catch (_b) {
-                    // Intentionally empty
+                catch (err) {
+                    core.debug(`${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
+                    core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`);
                 }
             }
             // SSH command
+            core.info('Removing SSH command configuration');
             yield this.removeGitConfig(SSH_COMMAND_KEY);
+            yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY);
         });
     }
+    /**
+     * Removes token-based authentication by cleaning up HTTP headers,
+     * includeIf entries, and credentials config files.
+     */
     removeToken() {
         return __awaiter(this, void 0, void 0, function* () {
-            // HTTP extra header
+            var _a;
+            // Remove HTTP extra header
+            core.info('Removing HTTP extra header');
             yield this.removeGitConfig(this.tokenConfigKey);
+            yield this.removeSubmoduleGitConfig(this.tokenConfigKey);
+            // Collect credentials config paths that need to be removed
+            const credentialsPaths = new Set();
+            // Remove includeIf entries that point to git-credentials-*.config files
+            core.info('Removing includeIf entries pointing to credentials config files');
+            const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
+            mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
+            // Remove submodule includeIf entries that point to git-credentials-*.config files
+            const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true);
+            for (const configPath of submoduleConfigPaths) {
+                const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath);
+                submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path));
+            }
+            // Remove credentials config files
+            for (const credentialsPath of credentialsPaths) {
+                // Only remove credentials config files if they are under RUNNER_TEMP
+                const runnerTemp = process.env['RUNNER_TEMP'];
+                assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
+                if (credentialsPath.startsWith(runnerTemp)) {
+                    try {
+                        core.info(`Removing credentials config '${credentialsPath}'`);
+                        yield io.rmRF(credentialsPath);
+                    }
+                    catch (err) {
+                        core.debug(`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
+                        core.warning(`Failed to remove credentials config '${credentialsPath}'`);
+                    }
+                }
+                else {
+                    core.debug(`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`);
+                }
+            }
         });
     }
-    removeGitConfig(configKey_1) {
-        return __awaiter(this, arguments, void 0, function* (configKey, submoduleOnly = false) {
-            if (!submoduleOnly) {
-                if ((yield this.git.configExists(configKey)) &&
-                    !(yield this.git.tryConfigUnset(configKey))) {
-                    // Load the config contents
-                    core.warning(`Failed to remove '${configKey}' from the git config`);
-                }
+    /**
+     * Removes a git config key from the local repository config.
+     * @param configKey The git config key to remove
+     */
+    removeGitConfig(configKey) {
+        return __awaiter(this, void 0, void 0, function* () {
+            if ((yield this.git.configExists(configKey)) &&
+                !(yield this.git.tryConfigUnset(configKey))) {
+                // Load the config contents
+                core.warning(`Failed to remove '${configKey}' from the git config`);
             }
+        });
+    }
+    /**
+     * Removes a git config key from all submodule configs.
+     * @param configKey The git config key to remove
+     */
+    removeSubmoduleGitConfig(configKey) {
+        return __awaiter(this, void 0, void 0, function* () {
             const pattern = regexpHelper.escape(configKey);
             yield this.git.submoduleForeach(
-            // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
+            // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
             `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
         });
     }
+    /**
+     * Removes includeIf entries that point to git-credentials-*.config files.
+     * @param configPath Optional path to a specific git config file to operate on
+     * @returns Array of unique credentials config file paths that were found and removed
+     */
+    removeIncludeIfCredentials(configPath) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const credentialsPaths = new Set();
+            try {
+                // Get all includeIf.gitdir keys
+                const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
+                configPath);
+                for (const key of keys) {
+                    // Get all values for this key
+                    const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
+                    configPath);
+                    if (values.length > 0) {
+                        // Remove only values that match git-credentials-<uuid>.config pattern
+                        for (const value of values) {
+                            if (this.testCredentialsConfigPath(value)) {
+                                credentialsPaths.add(value);
+                                yield this.git.tryConfigUnsetValue(key, value, false, configPath);
+                            }
+                        }
+                    }
+                }
+            }
+            catch (err) {
+                // Ignore errors - this is cleanup code
+                if (configPath) {
+                    core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`);
+                }
+                else {
+                    core.debug(`Error during includeIf cleanup: ${err}`);
+                }
+            }
+            return Array.from(credentialsPaths);
+        });
+    }
+    /**
+     * Tests if a path matches the git-credentials-*.config pattern.
+     * @param path The path to test
+     * @returns True if the path matches the credentials config pattern
+     */
+    testCredentialsConfigPath(path) {
+        return /git-credentials-[0-9a-f-]+\.config$/i.test(path);
+    }
 }
 
 
@@ -627,9 +799,15 @@ class GitCommandManager {
             yield this.execGit(args);
         });
     }
-    config(configKey, configValue, globalConfig, add) {
+    config(configKey, configValue, globalConfig, add, configFile) {
         return __awaiter(this, void 0, void 0, function* () {
-            const args = ['config', globalConfig ? '--global' : '--local'];
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
             if (add) {
                 args.push('--add');
             }
@@ -706,6 +884,16 @@ class GitCommandManager {
             throw new Error('Unexpected output when retrieving default branch');
         });
     }
+    getSubmoduleConfigPaths(recursive) {
+        return __awaiter(this, void 0, void 0, function* () {
+            // Get submodule config file paths.
+            // Use `--show-origin` to get the config file path for each submodule.
+            const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive);
+            // Extract config file paths from the output (lines starting with "file:").
+            const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+            return configPaths;
+        });
+    }
     getWorkingDirectory() {
         return this.workingDirectory;
     }
@@ -836,6 +1024,20 @@ class GitCommandManager {
             return output.exitCode === 0;
         });
     }
+    tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--unset', configKey, configValue);
+            const output = yield this.execGit(args, true);
+            return output.exitCode === 0;
+        });
+    }
     tryDisableAutomaticGarbageCollection() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
@@ -855,6 +1057,46 @@ class GitCommandManager {
             return stdout;
         });
     }
+    tryGetConfigValues(configKey, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--get-all', configKey);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(value => value.trim());
+        });
+    }
+    tryGetConfigKeys(pattern, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--name-only', '--get-regexp', pattern);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(key => key.trim());
+        });
+    }
     tryReset() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
diff --git package.json package.json
index dbbaabbae..4b2b58a51 100644
--- package.json
+++ package.json
@@ -1,6 +1,6 @@
 {
   "name": "checkout",
-  "version": "4.3.0",
+  "version": "5.0.0",
   "description": "checkout action",
   "main": "lib/main.js",
   "scripts": {
@@ -37,7 +37,7 @@
   },
   "devDependencies": {
     "@types/jest": "^29.5.12",
-    "@types/node": "^20.12.12",
+    "@types/node": "^24.1.0",
     "@types/uuid": "^9.0.8",
     "@typescript-eslint/eslint-plugin": "^7.9.0",
     "@typescript-eslint/parser": "^7.9.0",
diff --git src/git-auth-helper.ts src/git-auth-helper.ts
index 126e8e5ee..e67db148a 100644
--- src/git-auth-helper.ts
+++ src/git-auth-helper.ts
@@ -43,6 +43,7 @@ class GitAuthHelper {
   private sshKeyPath = ''
   private sshKnownHostsPath = ''
   private temporaryHomePath = ''
+  private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
 
   constructor(
     gitCommand,Manager: IGitCommandManager,
@@ -126,16 +127,21 @@ class GitAuthHelper {
 
   async configureGlobalAuth(): Promise<void> {
     // 'configureTempGlobalConfig' noops if already set, just returns the path
-    const newGitConfigPath = await this.configureTempGlobalConfig()
+    await this.configureTempGlobalConfig()
     try {
       // Configure the token
-      await this.configureToken(newGitConfigPath, true)
+      await this.configureToken(true)
 
       // Configure HTTPS instead of SSH
       await this.git.tryConfigUnset(this.insteadOfKey, true)
       if (!this.settings.sshKey) {
         for (const insteadOfValue of this.insteadOfValues) {
-          await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
+          await this.git.config(
+            this.insteadOfKey,
+            insteadOfValue,
+            true, // globalConfig?
+            true // add?
+          )
         }
       }
     } catch (err) {
@@ -150,24 +156,60 @@ class GitAuthHelper {
 
   async configureSubmoduleAuth(): Promise<void> {
     // Remove possible previous HTTPS instead of SSH
-    await this.removeGitConfig(this.insteadOfKey, true)
+    await this.removeSubmoduleGitConfig(this.insteadOfKey)
 
     if (this.settings.persistCredentials) {
-      // Configure a placeholder value. This approach avoids the credential being captured
-      // by process creation audit events, which are commonly logged. For more information,
-      // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
-      const output = await this.git.submoduleForeach(
-        // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
-        `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
+      // Get the credentials config file path in RUNNER_TEMP
+      const credentialsConfigPath = this.getCredentialsConfigPath()
+
+      // Container credentials config path
+      const containerCredentialsPath = path.posix.join(
+        '/github/runner_temp',
+        path.basename(credentialsConfigPath)
+      )
+
+      // Get submodule config file paths.
+      const configPaths = await this.git.getSubmoduleConfigPaths(
         this.settings.nestedSubmodules
       )
 
-      // Replace the placeholder
-      const configPaths: string[] =
-        output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+      // For each submodule, configure includeIf entries pointing to the shared credentials file.
+      // Configure both host and container paths to support Docker container actions.
       for (const configPath of configPaths) {
-        core.debug(`Replacing token placeholder in '${configPath}'`)
-        await this.replaceTokenPlaceholder(configPath)
+        // Submodule Git directory
+        let submoduleGitDir = path.dirname(configPath) // The config file is at .git/modules/submodule-name/config
+        submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+
+        // Configure host includeIf
+        await this.git.config(
+          `includeIf.gitdir:${submoduleGitDir}.path`,
+          credentialsConfigPath,
+          false, // globalConfig?
+          false, // add?
+          configPath
+        )
+
+        // Container submodule git directory
+        const githubWorkspace = process.env['GITHUB_WORKSPACE']
+        assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
+        let relativeSubmoduleGitDir = path.relative(
+          githubWorkspace,
+          submoduleGitDir
+        )
+        relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+        const containerSubmoduleGitDir = path.posix.join(
+          '/github/workspace',
+          relativeSubmoduleGitDir
+        )
+
+        // Configure container includeIf
+        await this.git.config(
+          `includeIf.gitdir:${containerSubmoduleGitDir}.path`,
+          containerCredentialsPath,
+          false, // globalConfig?
+          false, // add?
+          configPath
+        )
       }
 
       if (this.settings.sshKey) {
@@ -201,6 +243,10 @@ class GitAuthHelper {
     }
   }
 
+  /**
+   * Configures SSH authentication by writing the SSH key and known hosts,
+   * and setting up the GIT_SSH_COMMAND environment variable.
+   */
   private async configureSsh(): Promise<void> {
     if (!this.settings.sshKey) {
       return
@@ -272,57 +318,127 @@ class GitAuthHelper {
     }
   }
 
-  private async configureToken(
-    configPath?: string,
-    globalConfig?: boolean
-  ): Promise<void> {
-    // Validate args
-    assert.ok(
-      (configPath && globalConfig) || (!configPath && !globalConfig),
-      'Unexpected configureToken parameter combinations'
-    )
-
-    // Default config path
-    if (!configPath && !globalConfig) {
-      configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
-    }
-
-    // Configure a placeholder value. This approach avoids the credential being captured
-    // by process creation audit events, which are commonly logged. For more information,
-    // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
+  /**
+   * Configures token-based authentication by creating a credentials config file
+   * and setting up includeIf entries to reference it.
+   * @param globalConfig Whether to configure global config instead of local
+   */
+  private async configureToken(globalConfig?: boolean): Promise<void> {
+    // Get the credentials config file path in RUNNER_TEMP
+    const credentialsConfigPath = this.getCredentialsConfigPath()
+
+    // Write placeholder to the separate credentials config file using git config.
+    // This approach avoids the credential being captured by process creation audit events,
+    // which are commonly logged. For more information, refer to
+    // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
     await this.git.config(
       this.tokenConfigKey,
       this.tokenPlaceholderConfigValue,
-      globalConfig
+      false, // globalConfig?
+      false, // add?
+      credentialsConfigPath
     )
 
-    // Replace the placeholder
-    await this.replaceTokenPlaceholder(configPath || '')
-  }
-
-  private async replaceTokenPlaceholder(configPath: string): Promise<void> {
-    assert.ok(configPath, 'configPath is not defined')
-    let content = (await fs.promises.readFile(configPath)).toString()
+    // Replace the placeholder in the credentials config file
+    let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
     const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
     if (
       placeholderIndex < 0 ||
       placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
     ) {
-      throw new Error(`Unable to replace auth placeholder in ${configPath}`)
+      throw new Error(
+        `Unable to replace auth placeholder in ${credentialsConfigPath}`
+      )
     }
     assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
     content = content.replace(
       this.tokenPlaceholderConfigValue,
       this.tokenConfigValue
     )
-    await fs.promises.writeFile(configPath, content)
+    await fs.promises.writeFile(credentialsConfigPath, content)
+
+    // Add include or includeIf to reference the credentials config
+    if (globalConfig) {
+      // Global config file is temporary
+      await this.git.config(
+        'include.path',
+        credentialsConfigPath,
+        true // globalConfig?
+      )
+    } else {
+      // Host git directory
+      let gitDir = path.join(this.git.getWorkingDirectory(), '.git')
+      gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+
+      // Configure host includeIf
+      const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
+      await this.git.config(hostIncludeKey, credentialsConfigPath)
+
+      // Configure host includeIf for worktrees
+      const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
+      await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
+
+      // Container git directory
+      const workingDirectory = this.git.getWorkingDirectory()
+      const githubWorkspace = process.env['GITHUB_WORKSPACE']
+      assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
+      let relativePath = path.relative(githubWorkspace, workingDirectory)
+      relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows
+      const containerGitDir = path.posix.join(
+        '/github/workspace',
+        relativePath,
+        '.git'
+      )
+
+      // Container credentials config path
+      const containerCredentialsPath = path.posix.join(
+        '/github/runner_temp',
+        path.basename(credentialsConfigPath)
+      )
+
+      // Configure container includeIf
+      const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
+      await this.git.config(containerIncludeKey, containerCredentialsPath)
+
+      // Configure container includeIf for worktrees
+      const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
+      await this.git.config(
+        containerWorktreeIncludeKey,
+        containerCredentialsPath
+      )
+    }
+  }
+
+  /**
+   * Gets or creates the path to the credentials config file in RUNNER_TEMP.
+   * @returns The absolute path to the credentials config file
+   */
+  private getCredentialsConfigPath(): string {
+    if (this.credentialsConfigPath) {
+      return this.credentialsConfigPath
+    }
+
+    const runnerTemp = process.env['RUNNER_TEMP'] || ''
+    assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
+
+    // Create a unique filename for this checkout instance
+    const configFileName = `git-credentials-${uuid()}.config`
+    this.credentialsConfigPath = path.join(runnerTemp, configFileName)
+
+    core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
+    return this.credentialsConfigPath
   }
 
+  /**
+   * Removes SSH authentication configuration by cleaning up SSH keys,
+   * known hosts files, and SSH command configurations.
+   */
   private async removeSsh(): Promise<void> {
     // SSH key
     const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
     if (keyPath) {
       try {
+        core.info(`Removing SSH key '${keyPath}'`)
         await io.rmRF(keyPath)
       } catch (err) {
         core.debug(`${(err as any)?.message ?? err}`)
@@ -335,40 +451,149 @@ class GitAuthHelper {
       this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
     if (knownHostsPath) {
       try {
+        core.info(`Removing SSH known hosts '${knownHostsPath}'`)
         await io.rmRF(knownHostsPath)
-      } catch {
-        // Intentionally empty
+      } catch (err) {
+        core.debug(`${(err as any)?.message ?? err}`)
+        core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
       }
     }
 
     // SSH command
+    core.info('Removing SSH command configuration')
     await this.removeGitConfig(SSH_COMMAND_KEY)
+    await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY)
   }
 
+  /**
+   * Removes token-based authentication by cleaning up HTTP headers,
+   * includeIf entries, and credentials config files.
+   */
   private async removeToken(): Promise<void> {
-    // HTTP extra header
+    // Remove HTTP extra header
+    core.info('Removing HTTP extra header')
     await this.removeGitConfig(this.tokenConfigKey)
-  }
+    await this.removeSubmoduleGitConfig(this.tokenConfigKey)
+
+    // Collect credentials config paths that need to be removed
+    const credentialsPaths = new Set<string>()
+
+    // Remove includeIf entries that point to git-credentials-*.config files
+    core.info('Removing includeIf entries pointing to credentials config files')
+    const mainCredentialsPaths = await this.removeIncludeIfCredentials()
+    mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
+
+    // Remove submodule includeIf entries that point to git-credentials-*.config files
+    const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true)
+    for (const configPath of submoduleConfigPaths) {
+      const submoduleCredentialsPaths =
+        await this.removeIncludeIfCredentials(configPath)
+      submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
+    }
 
-  private async removeGitConfig(
-    configKey: string,
-    submoduleOnly: boolean = false
-  ): Promise<void> {
-    if (!submoduleOnly) {
-      if (
-        (await this.git.configExists(configKey)) &&
-        !(await this.git.tryConfigUnset(configKey))
-      ) {
-        // Load the config contents
-        core.warning(`Failed to remove '${configKey}' from the git config`)
+    // Remove credentials config files
+    for (const credentialsPath of credentialsPaths) {
+      // Only remove credentials config files if they are under RUNNER_TEMP
+      const runnerTemp = process.env['RUNNER_TEMP']
+      assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
+      if (credentialsPath.startsWith(runnerTemp)) {
+        try {
+          core.info(`Removing credentials config '${credentialsPath}'`)
+          await io.rmRF(credentialsPath)
+        } catch (err) {
+          core.debug(`${(err as any)?.message ?? err}`)
+          core.warning(
+            `Failed to remove credentials config '${credentialsPath}'`
+          )
+        }
+      } else {
+        core.debug(
+          `Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`
+        )
       }
     }
+  }
+
+  /**
+   * Removes a git config key from the local repository config.
+   * @param configKey The git config key to remove
+   */
+  private async removeGitConfig(configKey: string): Promise<void> {
+    if (
+      (await this.git.configExists(configKey)) &&
+      !(await this.git.tryConfigUnset(configKey))
+    ) {
+      // Load the config contents
+      core.warning(`Failed to remove '${configKey}' from the git config`)
+    }
+  }
 
+  /**
+   * Removes a git config key from all submodule configs.
+   * @param configKey The git config key to remove
+   */
+  private async removeSubmoduleGitConfig(configKey: string): Promise<void> {
     const pattern = regexpHelper.escape(configKey)
     await this.git.submoduleForeach(
-      // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
+      // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
       `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
       true
     )
   }
+
+  /**
+   * Removes includeIf entries that point to git-credentials-*.config files.
+   * @param configPath Optional path to a specific git config file to operate on
+   * @returns Array of unique credentials config file paths that were found and removed
+   */
+  private async removeIncludeIfCredentials(
+    configPath?: string
+  ): Promise<string[]> {
+    const credentialsPaths = new Set<string>()
+
+    try {
+      // Get all includeIf.gitdir keys
+      const keys = await this.git.tryGetConfigKeys(
+        '^includeIf\\.gitdir:',
+        false, // globalConfig?
+        configPath
+      )
+
+      for (const key of keys) {
+        // Get all values for this key
+        const values = await this.git.tryGetConfigValues(
+          key,
+          false, // globalConfig?
+          configPath
+        )
+        if (values.length > 0) {
+          // Remove only values that match git-credentials-<uuid>.config pattern
+          for (const value of values) {
+            if (this.testCredentialsConfigPath(value)) {
+              credentialsPaths.add(value)
+              await this.git.tryConfigUnsetValue(key, value, false, configPath)
+            }
+          }
+        }
+      }
+    } catch (err) {
+      // Ignore errors - this is cleanup code
+      if (configPath) {
+        core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`)
+      } else {
+        core.debug(`Error during includeIf cleanup: ${err}`)
+      }
+    }
+
+    return Array.from(credentialsPaths)
+  }
+
+  /**
+   * Tests if a path matches the git-credentials-*.config pattern.
+   * @param path The path to test
+   * @returns True if the path matches the credentials config pattern
+   */
+  private testCredentialsConfigPath(path: string): boolean {
+    return /git-credentials-[0-9a-f-]+\.config$/i.test(path)
+  }
 }
diff --git src/git-command-manager.ts src/git-command-manager.ts
index 8e42a387f..a45e15a86 100644
--- src/git-command-manager.ts
+++ src/git-command-manager.ts
@@ -28,7 +28,8 @@ export interface IGitCommandManager {
     configKey: string,
     configValue: string,
     globalConfig?: boolean,
-    add?: boolean
+    add?: boolean,
+    configFile?: string
   ): Promise<void>
   configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
   fetch(
@@ -41,6 +42,7 @@ export interface IGitCommandManager {
     }
   ): Promise<void>
   getDefaultBranch(repositoryUrl: string): Promise<string>
+  getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
   getWorkingDirectory(): string
   init(): Promise<void>
   isDetached(): Promise<boolean>
@@ -59,8 +61,24 @@ export interface IGitCommandManager {
   tagExists(pattern: string): Promise<boolean>
   tryClean(): Promise<boolean>
   tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
+  tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean>
   tryDisableAutomaticGarbageCollection(): Promise<boolean>
   tryGetFetchUrl(): Promise<string>
+  tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
+  tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
   tryReset(): Promise<boolean>
   version(): Promise<GitVersion>
 }
@@ -223,9 +241,15 @@ class GitCommandManager {
     configKey: string,
     configValue: string,
     globalConfig?: boolean,
-    add?: boolean
+    add?: boolean,
+    configFile?: string
   ): Promise<void> {
-    const args: string[] = ['config', globalConfig ? '--global' : '--local']
+    const args: string[] = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
     if (add) {
       args.push('--add')
     }
@@ -323,6 +347,21 @@ class GitCommandManager {
     throw new Error('Unexpected output when retrieving default branch')
   }
 
+  async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
+    // Get submodule config file paths.
+    // Use `--show-origin` to get the config file path for each submodule.
+    const output = await this.submoduleForeach(
+      `git config --local --show-origin --name-only --get-regexp remote.origin.url`,
+      recursive
+    )
+
+    // Extract config file paths from the output (lines starting with "file:").
+    const configPaths =
+      output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+
+    return configPaths
+  }
+
   getWorkingDirectory(): string {
     return this.workingDirectory
   }
@@ -455,6 +494,24 @@ class GitCommandManager {
     return output.exitCode === 0
   }
 
+  async tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--unset', configKey, configValue)
+
+    const output = await this.execGit(args, true)
+    return output.exitCode === 0
+  }
+
   async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
     const output = await this.execGit(
       ['config', '--local', 'gc.auto', '0'],
@@ -481,6 +538,56 @@ class GitCommandManager {
     return stdout
   }
 
+  async tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--get-all', configKey)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(value => value.trim())
+  }
+
+  async tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--name-only', '--get-regexp', pattern)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(key => key.trim())
+  }
+
   async tryReset(): Promise<boolean> {
     const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
     return output.exitCode === 0
diff --git src/misc/generate-docs.ts src/misc/generate-docs.ts
index 4b3c8ff52..b78f035c5 100644
--- src/misc/generate-docs.ts
+++ src/misc/generate-docs.ts
@@ -120,7 +120,7 @@ function updateUsage(
 }
 
 updateUsage(
-  'actions/checkout@v4',
+  'actions/checkout@v6',
   path.join(__dirname, '..', '..', 'action.yml'),
   path.join(__dirname, '..', '..', 'README.md')
 )

Description

This PR upgrades the checkout action from v4 to v6 with significant security improvements and updates. The main changes include:

  1. Improved credential security: Credentials are now stored in a separate config file under $RUNNER_TEMP instead of directly in .git/config, using git's includeIf mechanism
  2. Node.js runtime upgrade: Updated from Node.js 20 to Node.js 24
  3. Worktree support: Added tests and support for git worktrees with proper credential handling
  4. Container support improvements: Enhanced credential handling for Docker container actions with separate path configurations

Possible Issues

  1. Breaking change in Actions Runner requirement: The new version requires Actions Runner v2.327.1+ for Node.js 24 support and v2.329.0+ for Docker container actions. This could break workflows running on older self-hosted runners.

  2. File system permissions: The credentials config file is created in $RUNNER_TEMP with potentially sensitive data. If the runner temp directory has weak permissions, credentials could be exposed.

  3. Cleanup complexity: The cleanup logic has become significantly more complex with multiple config files and includeIf entries across multiple locations. If cleanup fails partially, credentials could persist in unexpected locations.

  4. Path handling inconsistencies: The code uses both Windows backslashes and POSIX forward slashes with conversions. On Windows systems, there could be edge cases where path matching fails for includeIf patterns.

Security Hotspots

  1. Credential file creation (HIGH RISK - src/git-auth-helper.ts:384-396):

    • Credentials are written to a file with a placeholder value first, then replaced
    • If the replacement fails mid-operation, the placeholder could remain or partial credentials could be written
    • No explicit file permission setting is visible - should verify the file is created with restrictive permissions (0600)
  2. Credential cleanup failure (MEDIUM RISK - src/git-auth-helper.ts:472-510):

    • Multiple cleanup operations that could fail independently
    • Uses try-catch blocks that continue on error, potentially leaving credentials in place
    • If RUNNER_TEMP check fails or path traversal occurs, credentials outside temp might not be cleaned up
  3. Submodule credential propagation (MEDIUM RISK - src/git-auth-helper.ts:256-286):

    • Credentials are configured for all submodules recursively
    • Each submodule gets includeIf entries pointing to the shared credentials file
    • If submodule path enumeration is manipulated, credentials could be exposed to unintended directories
  4. Path validation (LOW-MEDIUM RISK - src/git-auth-helper.ts:591-594):

    • The testCredentialsConfigPath regex pattern /git-credentials-[0-9a-f-]+\.config$/i is fairly broad
    • Could potentially match unintended files if malicious config entries are present
    • Should validate full path, not just filename pattern

Privacy Hotspots

  1. Credential logging (MEDIUM RISK):

    • Multiple core.info() and core.debug() calls that log file paths containing credentials
    • Example: core.info(\Removing credentials config '${credentialsPath}'`)`
    • While paths themselves aren't sensitive, they reveal the credential storage mechanism and could aid attackers
  2. Git config output exposure (LOW RISK - __test__/verify-worktree.sh:21-22):

    • Test scripts use git config --list --show-origin which could expose credential file paths in CI logs
    • While this is test code, it demonstrates commands that could leak information if used in production
Changes

Changes

Workflow Files (.github/workflows/*)

  • Updated all workflow references from actions/checkout@v4.1.6 to actions/checkout@v6
  • Updated Node.js version from 20.x to 24.x in test workflows
  • Added v5 to the major version update options

Test Files

  • __test__/git-auth-helper.test.ts:
    • Extensive new tests for credentials config file handling
    • Tests for includeIf entry creation and removal
    • Tests for submodule credential configuration
    • Added regex pattern validation tests for credentials config paths
  • __test__/verify-worktree.sh (NEW):
    • New test script to verify credentials work in git worktrees
    • Checks includeIf configuration and fetch operations in worktrees
  • __test__/verify-submodules-*.sh:
    • Updated to use --includes flag when checking git config

Source Files

  • src/git-auth-helper.ts:

    • credentialsConfigPath field added to store credentials file path
    • configureToken() refactored to create separate credentials file with includeIf entries for both host and container paths
    • getCredentialsConfigPath() generates unique credential file paths using UUIDs
    • removeToken() now removes includeIf entries and credentials files
    • removeIncludeIfCredentials() removes includeIf entries matching credentials pattern
    • testCredentialsConfigPath() validates credentials file path pattern
    • configureSubmoduleAuth() refactored to use includeIf instead of direct config writes
    • Enhanced cleanup methods with separate functions for main repo and submodules
  • src/git-command-manager.ts:

    • config() method now accepts optional configFile parameter
    • getSubmoduleConfigPaths() (NEW) extracts submodule config file paths
    • tryConfigUnsetValue() (NEW) removes specific config values
    • tryGetConfigValues() (NEW) retrieves all values for a config key
    • tryGetConfigKeys() (NEW) retrieves config keys matching a pattern

Configuration Files

  • action.yml: Changed from node20 to node24 runtime
  • package.json: Version bumped to 5.0.0, updated @types/node to v24
  • CHANGELOG.md: Added entries for v6.0.0, v5.0.1, v5.0.0, v4.3.1
  • README.md: Updated examples and added "What's new" sections for v5 and v6

Distribution

  • dist/index.js: Compiled output reflecting all source changes
sequenceDiagram
    participant Workflow
    participant CheckoutAction
    participant GitAuthHelper
    participant GitCommandManager
    participant FileSystem
    participant GitConfig

    Workflow->>CheckoutAction: Execute checkout@v6
    CheckoutAction->>GitAuthHelper: configureAuth()
    
    alt Token-based auth
        GitAuthHelper->>GitAuthHelper: getCredentialsConfigPath()
        GitAuthHelper->>FileSystem: Create unique credentials file in RUNNER_TEMP
        GitAuthHelper->>GitCommandManager: config(key, placeholder, file=credentialsFile)
        GitCommandManager->>FileSystem: Write placeholder to credentials file
        GitAuthHelper->>FileSystem: Replace placeholder with actual token
        
        alt Local config
            GitAuthHelper->>GitCommandManager: config(includeIf.gitdir:host_path)
            GitCommandManager->>GitConfig: Add includeIf entry for host
            GitAuthHelper->>GitCommandManager: config(includeIf.gitdir:container_path)
            GitCommandManager->>GitConfig: Add includeIf entry for container
            GitAuthHelper->>GitCommandManager: config(includeIf.gitdir:worktrees)
            GitCommandManager->>GitConfig: Add includeIf entries for worktrees
        else Global config
            GitAuthHelper->>GitCommandManager: config(include.path, credentialsFile)
            GitCommandManager->>GitConfig: Add include.path to global config
        end
        
        alt With submodules
            GitAuthHelper->>GitCommandManager: getSubmoduleConfigPaths()
            GitCommandManager-->>GitAuthHelper: Return submodule config paths
            loop For each submodule
                GitAuthHelper->>GitCommandManager: config(includeIf, file=submoduleConfig)
                GitCommandManager->>GitConfig: Add includeIf to submodule config
            end
        end
    else SSH-based auth
        GitAuthHelper->>FileSystem: Write SSH key to RUNNER_TEMP
        GitAuthHelper->>FileSystem: Write known_hosts to RUNNER_TEMP
        GitAuthHelper->>GitConfig: Set core.sshCommand
    end
    
    CheckoutAction->>GitCommandManager: fetch/checkout operations
    
    Note over CheckoutAction,GitConfig: Post-cleanup phase
    
    CheckoutAction->>GitAuthHelper: removeAuth()
    GitAuthHelper->>GitAuthHelper: removeToken()
    GitAuthHelper->>GitCommandManager: tryGetConfigKeys(includeIf.gitdir)
    GitCommandManager-->>GitAuthHelper: Return includeIf keys
    loop For each includeIf entry
        GitAuthHelper->>GitCommandManager: tryGetConfigValues(key)
        GitCommandManager-->>GitAuthHelper: Return credential paths
        GitAuthHelper->>GitCommandManager: tryConfigUnsetValue(key, value)
        GitCommandManager->>GitConfig: Remove includeIf entry
    end
    GitAuthHelper->>FileSystem: Delete credentials config file
    
    alt With submodules
        loop For each submodule
            GitAuthHelper->>GitCommandManager: tryGetConfigKeys(includeIf, file=submodule)
            GitAuthHelper->>GitCommandManager: tryConfigUnsetValue()
        end
    end
Loading

@renovate renovate bot force-pushed the renovate/actions-checkout-6-x branch from cc2799c to 1d6da89 Compare January 23, 2026 08:10
@socket-security
Copy link

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: github actions/checkout is 98.0% likely obfuscated

Confidence: 0.98

Location: Package overview

From: .github/workflows/codeql-analysis.ymlgithub/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore github/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: github actions/checkout is 98.0% likely obfuscated

Confidence: 0.98

Location: Package overview

From: .github/workflows/codeql-analysis.ymlgithub/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore github/actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@github-actions
Copy link

[puLL-Merge] - actions/checkout@v4.3.1..v6.0.2

Diff
diff --git .github/workflows/check-dist.yml .github/workflows/check-dist.yml
index 53902eeb9..c7d49620f 100644
--- .github/workflows/check-dist.yml
+++ .github/workflows/check-dist.yml
@@ -22,12 +22,12 @@ jobs:
     runs-on: ubuntu-latest
 
     steps:
-      - uses: actions/checkout@v4.1.6
+      - uses: actions/checkout@v6
 
-      - name: Set Node.js 20.x
+      - name: Set Node.js 24.x
         uses: actions/setup-node@v4
         with:
-          node-version: 20.x
+          node-version: 24.x
 
       - name: Install dependencies
         run: npm ci
diff --git .github/workflows/codeql-analysis.yml .github/workflows/codeql-analysis.yml
index 778d474d8..377fae951 100644
--- .github/workflows/codeql-analysis.yml
+++ .github/workflows/codeql-analysis.yml
@@ -39,7 +39,7 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v4.1.6
+      uses: actions/checkout@v6
 
     - name: Initialize CodeQL
       uses: github/codeql-action/init@v3
diff --git .github/workflows/licensed.yml .github/workflows/licensed.yml
index 1f71aa749..36e70e2c1 100644
--- .github/workflows/licensed.yml
+++ .github/workflows/licensed.yml
@@ -9,6 +9,6 @@ jobs:
     runs-on: ubuntu-latest
     name: Check licenses
     steps:
-      - uses: actions/checkout@v4.1.6
+      - uses: actions/checkout@v6
       - run: npm ci
       - run: npm run licensed-check
\ No newline at end of file
diff --git .github/workflows/publish-immutable-actions.yml .github/workflows/publish-immutable-actions.yml
index 87c020728..44d571ba9 100644
--- .github/workflows/publish-immutable-actions.yml
+++ .github/workflows/publish-immutable-actions.yml
@@ -14,7 +14,7 @@ jobs:
 
     steps:
       - name: Checking out
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
       - name: Publish
         id: publish
         uses: actions/publish-immutable-action@0.0.3
diff --git .github/workflows/test.yml .github/workflows/test.yml
index cde9f060e..0383c88d7 100644
--- .github/workflows/test.yml
+++ .github/workflows/test.yml
@@ -18,8 +18,8 @@ jobs:
     steps:
       - uses: actions/setup-node@v4
         with:
-          node-version: 20.x
-      - uses: actions/checkout@v4.1.6
+          node-version: 24.x
+      - uses: actions/checkout@v6
       - run: npm ci
       - run: npm run build
       - run: npm run format-check
@@ -37,7 +37,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
 
       # Basic checkout
       - name: Checkout basic
@@ -87,6 +87,17 @@ jobs:
       - name: Verify fetch filter
         run: __test__/verify-fetch-filter.sh
 
+      # Fetch tags
+      - name: Checkout with fetch-tags
+        uses: ./
+        with:
+          ref: test-data/v2/basic
+          path: fetch-tags-test
+          fetch-tags: true
+      - name: Verify fetch-tags
+        shell: bash
+        run: __test__/verify-fetch-tags.sh
+
       # Sparse checkout
       - name: Sparse checkout
         uses: ./
@@ -165,6 +176,22 @@ jobs:
       - name: Verify submodules recursive
         run: __test__/verify-submodules-recursive.sh
 
+      # Worktree credentials
+      - name: Checkout for worktree test
+        uses: ./
+        with:
+          path: worktree-test
+      - name: Verify worktree credentials
+        shell: bash
+        run: __test__/verify-worktree.sh worktree-test worktree-branch
+
+      # Worktree credentials in container step
+      - name: Verify worktree credentials in container step
+        if: runner.os == 'Linux'
+        uses: docker://bitnami/git:latest
+        with:
+          args: bash __test__/verify-worktree.sh worktree-test container-worktree-branch
+
       # Basic checkout using REST API
       - name: Remove basic
         if: runner.os != 'windows'
@@ -202,7 +229,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
 
       # Basic checkout using git
       - name: Checkout basic
@@ -234,7 +261,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
 
       # Basic checkout using git
       - name: Checkout basic
@@ -264,7 +291,7 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
         with:
           path: localClone
 
@@ -291,8 +318,8 @@ jobs:
           git fetch --no-tags --depth=1 origin +refs/heads/main:refs/remotes/origin/main
 
       # needed to make checkout post cleanup succeed
-      - name: Fix Checkout v4
-        uses: actions/checkout@v4.1.6
+      - name: Fix Checkout v6
+        uses: actions/checkout@v6
         with:
           path: localClone
 
@@ -301,13 +328,16 @@ jobs:
     steps:
       # Clone this repo
       - name: Checkout
-        uses: actions/checkout@v4.1.6
+        uses: actions/checkout@v6
+        with:
+          path: actions-checkout
 
       # Basic checkout using git
       - name: Checkout basic
         id: checkout
-        uses: ./
+        uses: ./actions-checkout
         with:
+          path: cloned-using-local-action
           ref: test-data/v2/basic
 
       # Verify output
@@ -325,7 +355,3 @@ jobs:
             echo "Expected commit to be 82f71901cf8c021332310dcc8cdba84c4193ff5d"
             exit 1
           fi
-
-      # needed to make checkout post cleanup succeed
-      - name: Fix Checkout
-        uses: actions/checkout@v4.1.6
diff --git .github/workflows/update-main-version.yml .github/workflows/update-main-version.yml
index 7bec7d5a8..b3b23fe4e 100644
--- .github/workflows/update-main-version.yml
+++ .github/workflows/update-main-version.yml
@@ -11,6 +11,7 @@ on:
         type: choice
         description: The major version to update
         options:
+          - v5
           - v4
           - v3
           - v2
@@ -22,7 +23,7 @@ jobs:
     # Note this update workflow can also be used as a rollback tool.
     # For that reason, it's best to pin `actions/checkout` to a known, stable version
     # (typically, about two releases back).
-    - uses: actions/checkout@v4.1.6
+    - uses: actions/checkout@v6
       with:
         fetch-depth: 0
     - name: Git config
diff --git .github/workflows/update-test-ubuntu-git.yml .github/workflows/update-test-ubuntu-git.yml
index 5c252b98d..10e4dac93 100644
--- .github/workflows/update-test-ubuntu-git.yml
+++ .github/workflows/update-test-ubuntu-git.yml
@@ -26,7 +26,7 @@ jobs:
  
     steps:
       - name: Checkout repository
-        uses: actions/checkout@v4
+        uses: actions/checkout@v6
 
       # Use `docker/login-action` to log in to GHCR.io. 
       # Once published, the packages are scoped to the account defined here.
diff --git CHANGELOG.md CHANGELOG.md
index baf5c2d7e..6d5a6f302 100644
--- CHANGELOG.md
+++ CHANGELOG.md
@@ -1,6 +1,19 @@
 # Changelog
 
-## V4.3.0
+## v6.0.0
+* Persist creds to a separate file by @ericsciple in https://github.com/actions/checkout/pull/2286
+* Update README to include Node.js 24 support details and requirements by @salmanmkc in https://github.com/actions/checkout/pull/2248
+
+## v5.0.1
+* Port v6 cleanup to v5 by @ericsciple in https://github.com/actions/checkout/pull/2301
+
+## v5.0.0
+* Update actions checkout to use node 24 by @salmanmkc in https://github.com/actions/checkout/pull/2226
+
+## v4.3.1
+* Port v6 cleanup to v4 by @ericsciple in https://github.com/actions/checkout/pull/2305
+
+## v4.3.0
 * docs: update README.md by @motss in https://github.com/actions/checkout/pull/1971
 * Add internal repos for checking out multiple repositories by @mouismail in https://github.com/actions/checkout/pull/1977
 * Documentation update - add recommended permissions to Readme by @benwells in https://github.com/actions/checkout/pull/2043
diff --git README.md README.md
index 8969446c3..f0f65f9f6 100644
--- README.md
+++ README.md
@@ -1,6 +1,22 @@
 [![Build and Test](https://github.com/actions/checkout/actions/workflows/test.yml/badge.svg)](https://github.com/actions/checkout/actions/workflows/test.yml)
 
-# Checkout V4
+# Checkout v6
+
+## What's new
+
+- Improved credential security: `persist-credentials` now stores credentials in a separate file under `$RUNNER_TEMP` instead of directly in `.git/config`
+- No workflow changes required — `git fetch`, `git push`, etc. continue to work automatically
+- Running authenticated git commands from a [Docker container action](https://docs.github.com/actions/sharing-automations/creating-actions/creating-a-docker-container-action) requires Actions Runner [v2.329.0](https://github.com/actions/runner/releases/tag/v2.329.0) or later
+
+# Checkout v5
+
+## What's new
+
+- Updated to the node24 runtime
+  - This requires a minimum Actions Runner version of [v2.327.1](https://github.com/actions/runner/releases/tag/v2.327.1) to run.
+
+
+# Checkout v4
 
 This action checks-out your repository under `$GITHUB_WORKSPACE`, so your workflow can access it.
 
@@ -36,7 +52,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 <!-- start usage -->
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     # Repository name with owner. For example, actions/checkout
     # Default: ${{ github.repository }}
@@ -149,24 +165,33 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 # Scenarios
 
-- [Fetch only the root files](#Fetch-only-the-root-files)
-- [Fetch only the root files and `.github` and `src` folder](#Fetch-only-the-root-files-and-github-and-src-folder)
-- [Fetch only a single file](#Fetch-only-a-single-file)
-- [Fetch all history for all tags and branches](#Fetch-all-history-for-all-tags-and-branches)
-- [Checkout a different branch](#Checkout-a-different-branch)
-- [Checkout HEAD^](#Checkout-HEAD)
-- [Checkout multiple repos (side by side)](#Checkout-multiple-repos-side-by-side)
-- [Checkout multiple repos (nested)](#Checkout-multiple-repos-nested)
-- [Checkout multiple repos (private)](#Checkout-multiple-repos-private)
-- [Checkout pull request HEAD commit instead of merge commit](#Checkout-pull-request-HEAD-commit-instead-of-merge-commit)
-- [Checkout pull request on closed event](#Checkout-pull-request-on-closed-event)
-- [Push a commit using the built-in token](#Push-a-commit-using-the-built-in-token)
-- [Push a commit to a PR using the built-in token](#Push-a-commit-to-a-PR-using-the-built-in-token)
+- [Checkout V5](#checkout-v5)
+  - [What's new](#whats-new)
+- [Checkout V4](#checkout-v4)
+    - [Note](#note)
+- [What's new](#whats-new-1)
+- [Usage](#usage)
+- [Scenarios](#scenarios)
+  - [Fetch only the root files](#fetch-only-the-root-files)
+  - [Fetch only the root files and `.github` and `src` folder](#fetch-only-the-root-files-and-github-and-src-folder)
+  - [Fetch only a single file](#fetch-only-a-single-file)
+  - [Fetch all history for all tags and branches](#fetch-all-history-for-all-tags-and-branches)
+  - [Checkout a different branch](#checkout-a-different-branch)
+  - [Checkout HEAD^](#checkout-head)
+  - [Checkout multiple repos (side by side)](#checkout-multiple-repos-side-by-side)
+  - [Checkout multiple repos (nested)](#checkout-multiple-repos-nested)
+  - [Checkout multiple repos (private)](#checkout-multiple-repos-private)
+  - [Checkout pull request HEAD commit instead of merge commit](#checkout-pull-request-head-commit-instead-of-merge-commit)
+  - [Checkout pull request on closed event](#checkout-pull-request-on-closed-event)
+  - [Push a commit using the built-in token](#push-a-commit-using-the-built-in-token)
+  - [Push a commit to a PR using the built-in token](#push-a-commit-to-a-pr-using-the-built-in-token)
+- [Recommended permissions](#recommended-permissions)
+- [License](#license)
 
 ## Fetch only the root files
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     sparse-checkout: .
 ```
@@ -174,7 +199,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch only the root files and `.github` and `src` folder
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     sparse-checkout: |
       .github
@@ -184,7 +209,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch only a single file
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     sparse-checkout: |
       README.md
@@ -194,7 +219,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Fetch all history for all tags and branches
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     fetch-depth: 0
 ```
@@ -202,7 +227,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout a different branch
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     ref: my-branch
 ```
@@ -210,7 +235,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout HEAD^
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     fetch-depth: 2
 - run: git checkout HEAD^
@@ -220,12 +245,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
   with:
     path: main
 
 - name: Checkout tools repo
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
   with:
     repository: my-org/my-tools
     path: my-tools
@@ -236,10 +261,10 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
 
 - name: Checkout tools repo
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
   with:
     repository: my-org/my-tools
     path: my-tools
@@ -250,12 +275,12 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 
 ```yaml
 - name: Checkout
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
   with:
     path: main
 
 - name: Checkout private tools
-  uses: actions/checkout@v4
+  uses: actions/checkout@v6
   with:
     repository: my-org/my-private-tools
     token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT
@@ -268,7 +293,7 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
 ## Checkout pull request HEAD commit instead of merge commit
 
 ```yaml
-- uses: actions/checkout@v4
+- uses: actions/checkout@v6
   with:
     ref: ${{ github.event.pull_request.head.sha }}
 ```
@@ -284,7 +309,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v6
 ```
 
 ## Push a commit using the built-in token
@@ -295,7 +320,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v6
       - run: |
           date > generated.txt
           # Note: the following account information will not work on GHES
@@ -317,7 +342,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v6
         with:
           ref: ${{ github.head_ref }}
       - run: |
diff --git __test__/git-auth-helper.test.ts __test__/git-auth-helper.test.ts
index 7633704cc..ad3566ad6 100644
--- __test__/git-auth-helper.test.ts
+++ __test__/git-auth-helper.test.ts
@@ -86,16 +86,29 @@ describe('git-auth-helper tests', () => {
     // Act
     await authHelper.configureAuth()
 
-    // Assert config
-    const configContent = (
+    // Assert config - check that .git/config contains includeIf entries
+    const localConfigContent = (
       await fs.promises.readFile(localGitConfigPath)
     ).toString()
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:')
+    ).toBeGreaterThanOrEqual(0)
+
+    // Assert credentials config file contains the actual credentials
+    const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(1)
+    const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+    const credentialsContent = (
+      await fs.promises.readFile(credentialsConfigPath)
+    ).toString()
     const basicCredential = Buffer.from(
       `x-access-token:${settings.authToken}`,
       'utf8'
     ).toString('base64')
     expect(
-      configContent.indexOf(
+      credentialsContent.indexOf(
         `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
       )
     ).toBeGreaterThanOrEqual(0)
@@ -120,7 +133,7 @@ describe('git-auth-helper tests', () => {
     'inject https://github.com as github server url'
   it(configureAuth_AcceptsGitHubServerUrlSetToGHEC, async () => {
     await testAuthHeader(
-      configureAuth_AcceptsGitHubServerUrl,
+      configureAuth_AcceptsGitHubServerUrlSetToGHEC,
       'https://github.com'
     )
   })
@@ -141,12 +154,17 @@ describe('git-auth-helper tests', () => {
       // Act
       await authHelper.configureAuth()
 
-      // Assert config
-      const configContent = (
-        await fs.promises.readFile(localGitConfigPath)
+      // Assert config - check credentials config file (not local .git/config)
+      const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+        f => f.startsWith('git-credentials-') && f.endsWith('.config')
+      )
+      expect(credentialsFiles.length).toBe(1)
+      const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+      const credentialsContent = (
+        await fs.promises.readFile(credentialsConfigPath)
       ).toString()
       expect(
-        configContent.indexOf(
+        credentialsContent.indexOf(
           `http.https://github.com/.extraheader AUTHORIZATION`
         )
       ).toBeGreaterThanOrEqual(0)
@@ -251,13 +269,16 @@ describe('git-auth-helper tests', () => {
       expectedSshCommand
     )
 
-    // Asserty git config
+    // Assert git config
     const gitConfigLines = (await fs.promises.readFile(localGitConfigPath))
       .toString()
       .split('\n')
       .filter(x => x)
-    expect(gitConfigLines).toHaveLength(1)
-    expect(gitConfigLines[0]).toMatch(/^http\./)
+    // Should have includeIf entries pointing to credentials file
+    expect(gitConfigLines.length).toBeGreaterThan(0)
+    expect(
+      gitConfigLines.some(line => line.indexOf('includeIf.gitdir:') >= 0)
+    ).toBeTruthy()
   })
 
   const configureAuth_setsSshCommandWhenPersistCredentialsTrue =
@@ -419,8 +440,20 @@ describe('git-auth-helper tests', () => {
     expect(
       configContent.indexOf('value-from-global-config')
     ).toBeGreaterThanOrEqual(0)
+    // Global config should have include.path pointing to credentials file
+    expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
+
+    // Check credentials in the separate config file
+    const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBeGreaterThan(0)
+    const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+    const credentialsContent = (
+      await fs.promises.readFile(credentialsConfigPath)
+    ).toString()
     expect(
-      configContent.indexOf(
+      credentialsContent.indexOf(
         `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
       )
     ).toBeGreaterThanOrEqual(0)
@@ -463,8 +496,20 @@ describe('git-auth-helper tests', () => {
       const configContent = (
         await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
       ).toString()
+      // Global config should have include.path pointing to credentials file
+      expect(configContent.indexOf('include.path')).toBeGreaterThanOrEqual(0)
+
+      // Check credentials in the separate config file
+      const credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+        f => f.startsWith('git-credentials-') && f.endsWith('.config')
+      )
+      expect(credentialsFiles.length).toBeGreaterThan(0)
+      const credentialsConfigPath = path.join(runnerTemp, credentialsFiles[0])
+      const credentialsContent = (
+        await fs.promises.readFile(credentialsConfigPath)
+      ).toString()
       expect(
-        configContent.indexOf(
+        credentialsContent.indexOf(
           `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
         )
       ).toBeGreaterThanOrEqual(0)
@@ -550,15 +595,15 @@ describe('git-auth-helper tests', () => {
       await authHelper.configureSubmoduleAuth()
 
       // Assert
-      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(4)
+      // Should configure insteadOf (2 calls for two values)
+      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
         /unset-all.*insteadOf/
       )
-      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
-      expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
+      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(
         /url.*insteadOf.*git@github.com:/
       )
-      expect(mockSubmoduleForeach.mock.calls[3][0]).toMatch(
+      expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
         /url.*insteadOf.*org-123456@github.com:/
       )
     }
@@ -589,12 +634,12 @@ describe('git-auth-helper tests', () => {
       await authHelper.configureSubmoduleAuth()
 
       // Assert
-      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(3)
+      // Should configure sshCommand (1 call)
+      expect(mockSubmoduleForeach).toHaveBeenCalledTimes(2)
       expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
         /unset-all.*insteadOf/
       )
-      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
-      expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
+      expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/core\.sshCommand/)
     }
   )
 
@@ -660,19 +705,201 @@ describe('git-auth-helper tests', () => {
     await setup(removeAuth_removesToken)
     const authHelper = gitAuthHelper.createAuthHelper(git, settings)
     await authHelper.configureAuth()
-    let gitConfigContent = (
+
+    // Verify includeIf entries exist in local config
+    let localConfigContent = (
       await fs.promises.readFile(localGitConfigPath)
     ).toString()
-    expect(gitConfigContent.indexOf('http.')).toBeGreaterThanOrEqual(0) // sanity check
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:')
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify both host and container includeIf entries are present
+    const hostGitDir = path.join(workspace, '.git').replace(/\\/g, '/')
+    expect(
+      localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify credentials file exists
+    let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(1)
+    const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
+
+    // Verify credentials file contains the auth token
+    let credentialsContent = (
+      await fs.promises.readFile(credentialsFilePath)
+    ).toString()
+    const basicCredential = Buffer.from(
+      `x-access-token:${settings.authToken}`,
+      'utf8'
+    ).toString('base64')
+    expect(
+      credentialsContent.indexOf(
+        `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
+      )
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify the includeIf entries point to the credentials file
+    const containerCredentialsPath = path.posix.join(
+      '/github/runner_temp',
+      path.basename(credentialsFilePath)
+    )
+    expect(
+      localConfigContent.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      localConfigContent.indexOf(containerCredentialsPath)
+    ).toBeGreaterThanOrEqual(0)
 
     // Act
     await authHelper.removeAuth()
 
-    // Assert git config
-    gitConfigContent = (
+    // Assert all includeIf entries removed from local git config
+    localConfigContent = (
       await fs.promises.readFile(localGitConfigPath)
     ).toString()
-    expect(gitConfigContent.indexOf('http.')).toBeLessThan(0)
+    expect(localConfigContent.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+    expect(
+      localConfigContent.indexOf(`includeIf.gitdir:${hostGitDir}.path`)
+    ).toBeLessThan(0)
+    expect(
+      localConfigContent.indexOf('includeIf.gitdir:/github/workspace/.git.path')
+    ).toBeLessThan(0)
+    expect(localConfigContent.indexOf(credentialsFilePath)).toBeLessThan(0)
+    expect(localConfigContent.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+    // Assert credentials config file deleted
+    credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(0)
+
+    // Verify credentials file no longer exists on disk
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
+  })
+
+  const removeAuth_removesTokenFromSubmodules =
+    'removeAuth removes token from submodules'
+  it(removeAuth_removesTokenFromSubmodules, async () => {
+    // Arrange
+    await setup(removeAuth_removesTokenFromSubmodules)
+
+    // Create fake submodule config paths
+    const submodule1Dir = path.join(workspace, '.git', 'modules', 'submodule-1')
+    const submodule2Dir = path.join(workspace, '.git', 'modules', 'submodule-2')
+    const submodule1ConfigPath = path.join(submodule1Dir, 'config')
+    const submodule2ConfigPath = path.join(submodule2Dir, 'config')
+
+    await fs.promises.mkdir(submodule1Dir, {recursive: true})
+    await fs.promises.mkdir(submodule2Dir, {recursive: true})
+    await fs.promises.writeFile(submodule1ConfigPath, '')
+    await fs.promises.writeFile(submodule2ConfigPath, '')
+
+    // Mock getSubmoduleConfigPaths to return our fake submodules (for both configure and remove)
+    const mockGetSubmoduleConfigPaths =
+      git.getSubmoduleConfigPaths as jest.Mock<any, any>
+    mockGetSubmoduleConfigPaths.mockResolvedValue([
+      submodule1ConfigPath,
+      submodule2ConfigPath
+    ])
+
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+    await authHelper.configureAuth()
+    await authHelper.configureSubmoduleAuth()
+
+    // Verify credentials file exists
+    let credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(1)
+    const credentialsFilePath = path.join(runnerTemp, credentialsFiles[0])
+
+    // Verify submodule 1 config has includeIf entries
+    let submodule1Content = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    const submodule1GitDir = submodule1Dir.replace(/\\/g, '/')
+    expect(
+      submodule1Content.indexOf(`includeIf.gitdir:${submodule1GitDir}.path`)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      submodule1Content.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify submodule 2 config has includeIf entries
+    let submodule2Content = (
+      await fs.promises.readFile(submodule2ConfigPath)
+    ).toString()
+    const submodule2GitDir = submodule2Dir.replace(/\\/g, '/')
+    expect(
+      submodule2Content.indexOf(`includeIf.gitdir:${submodule2GitDir}.path`)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      submodule2Content.indexOf(credentialsFilePath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Verify both host and container paths are in each submodule config
+    const containerCredentialsPath = path.posix.join(
+      '/github/runner_temp',
+      path.basename(credentialsFilePath)
+    )
+    expect(
+      submodule1Content.indexOf(containerCredentialsPath)
+    ).toBeGreaterThanOrEqual(0)
+    expect(
+      submodule2Content.indexOf(containerCredentialsPath)
+    ).toBeGreaterThanOrEqual(0)
+
+    // Act - ensure mock persists for removeAuth
+    mockGetSubmoduleConfigPaths.mockResolvedValue([
+      submodule1ConfigPath,
+      submodule2ConfigPath
+    ])
+    await authHelper.removeAuth()
+
+    // Assert submodule 1 includeIf entries removed
+    submodule1Content = (
+      await fs.promises.readFile(submodule1ConfigPath)
+    ).toString()
+    expect(submodule1Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+    expect(submodule1Content.indexOf(credentialsFilePath)).toBeLessThan(0)
+    expect(submodule1Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+    // Assert submodule 2 includeIf entries removed
+    submodule2Content = (
+      await fs.promises.readFile(submodule2ConfigPath)
+    ).toString()
+    expect(submodule2Content.indexOf('includeIf.gitdir:')).toBeLessThan(0)
+    expect(submodule2Content.indexOf(credentialsFilePath)).toBeLessThan(0)
+    expect(submodule2Content.indexOf(containerCredentialsPath)).toBeLessThan(0)
+
+    // Assert credentials config file deleted
+    credentialsFiles = (await fs.promises.readdir(runnerTemp)).filter(
+      f => f.startsWith('git-credentials-') && f.endsWith('.config')
+    )
+    expect(credentialsFiles.length).toBe(0)
+
+    // Verify credentials file no longer exists on disk
+    try {
+      await fs.promises.stat(credentialsFilePath)
+      throw new Error('Credentials file should have been deleted')
+    } catch (err) {
+      if ((err as any)?.code !== 'ENOENT') {
+        throw err
+      }
+    }
   })
 
   const removeGlobalConfig_removesOverride =
@@ -701,6 +928,52 @@ describe('git-auth-helper tests', () => {
       }
     }
   })
+
+  const testCredentialsConfigPath_matchesCredentialsConfigPaths =
+    'testCredentialsConfigPath matches credentials config paths'
+  it(testCredentialsConfigPath_matchesCredentialsConfigPaths, async () => {
+    // Arrange
+    await setup(testCredentialsConfigPath_matchesCredentialsConfigPaths)
+    const authHelper = gitAuthHelper.createAuthHelper(git, settings)
+
+    // Get a real credentials config path
+    const credentialsConfigPath = await (
+      authHelper as any
+    ).getCredentialsConfigPath()
+
+    // Act & Assert
+    expect(
+      (authHelper as any).testCredentialsConfigPath(credentialsConfigPath)
+    ).toBe(true)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-12345678-abcd-1234-5678-123456789012.config'
+      )
+    ).toBe(true)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-abcdef12-3456-7890-abcd-ef1234567890.config'
+      )
+    ).toBe(true)
+
+    // Test invalid paths
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/other-config.config'
+      )
+    ).toBe(false)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-invalid.config'
+      )
+    ).toBe(false)
+    expect(
+      (authHelper as any).testCredentialsConfigPath(
+        '/some/path/git-credentials-.config'
+      )
+    ).toBe(false)
+    expect((authHelper as any).testCredentialsConfigPath('')).toBe(false)
+  })
 })
 
 async function setup(testName: string): Promise<void> {
@@ -715,6 +988,7 @@ async function setup(testName: string): Promise<void> {
   await fs.promises.mkdir(tempHomedir, {recursive: true})
   process.env['RUNNER_TEMP'] = runnerTemp
   process.env['HOME'] = tempHomedir
+  process.env['GITHUB_WORKSPACE'] = workspace
 
   // Create git config
   globalGitConfigPath = path.join(tempHomedir, '.gitconfig')
@@ -733,10 +1007,20 @@ async function setup(testName: string): Promise<void> {
     checkout: jest.fn(),
     checkoutDetach: jest.fn(),
     config: jest.fn(
-      async (key: string, value: string, globalConfig?: boolean) => {
-        const configPath = globalConfig
-          ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
-          : localGitConfigPath
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        add?: boolean,
+        configFile?: string
+      ) => {
+        const configPath =
+          configFile ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        // Ensure directory exists
+        await fs.promises.mkdir(path.dirname(configPath), {recursive: true})
         await fs.promises.appendFile(configPath, `\n${key} ${value}`)
       }
     ),
@@ -756,6 +1040,7 @@ async function setup(testName: string): Promise<void> {
     env: {},
     fetch: jest.fn(),
     getDefaultBranch: jest.fn(),
+    getSubmoduleConfigPaths: jest.fn(async () => []),
     getWorkingDirectory: jest.fn(() => workspace),
     init: jest.fn(),
     isDetached: jest.fn(),
@@ -794,8 +1079,72 @@ async function setup(testName: string): Promise<void> {
         return true
       }
     ),
+    tryConfigUnsetValue: jest.fn(
+      async (
+        key: string,
+        value: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ): Promise<boolean> => {
+        const targetConfigPath =
+          configPath ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        let content = await fs.promises.readFile(targetConfigPath)
+        let lines = content
+          .toString()
+          .split('\n')
+          .filter(x => x)
+          .filter(x => !(x.startsWith(key) && x.includes(value)))
+        await fs.promises.writeFile(targetConfigPath, lines.join('\n'))
+        return true
+      }
+    ),
     tryDisableAutomaticGarbageCollection: jest.fn(),
     tryGetFetchUrl: jest.fn(),
+    tryGetConfigValues: jest.fn(
+      async (
+        key: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ): Promise<string[]> => {
+        const targetConfigPath =
+          configPath ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        const content = await fs.promises.readFile(targetConfigPath)
+        const lines = content
+          .toString()
+          .split('\n')
+          .filter(x => x && x.startsWith(key))
+          .map(x => x.substring(key.length).trim())
+        return lines
+      }
+    ),
+    tryGetConfigKeys: jest.fn(
+      async (
+        pattern: string,
+        globalConfig?: boolean,
+        configPath?: string
+      ): Promise<string[]> => {
+        const targetConfigPath =
+          configPath ||
+          (globalConfig
+            ? path.join(git.env['HOME'] || tempHomedir, '.gitconfig')
+            : localGitConfigPath)
+        const content = await fs.promises.readFile(targetConfigPath)
+        const lines = content
+          .toString()
+          .split('\n')
+          .filter(x => x)
+        const keys = lines
+          .filter(x => new RegExp(pattern).test(x.split(' ')[0]))
+          .map(x => x.split(' ')[0])
+        return [...new Set(keys)] // Remove duplicates
+      }
+    ),
     tryReset: jest.fn(),
     version: jest.fn()
   }
@@ -830,6 +1179,7 @@ async function setup(testName: string): Promise<void> {
 
 async function getActualSshKeyPath(): Promise<string> {
   let actualTempFiles = (await fs.promises.readdir(runnerTemp))
+    .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
     .sort()
     .map(x => path.join(runnerTemp, x))
   if (actualTempFiles.length === 0) {
@@ -843,6 +1193,7 @@ async function getActualSshKeyPath(): Promise<string> {
 
 async function getActualSshKnownHostsPath(): Promise<string> {
   let actualTempFiles = (await fs.promises.readdir(runnerTemp))
+    .filter(x => !x.startsWith('git-credentials-')) // Exclude credentials config file
     .sort()
     .map(x => path.join(runnerTemp, x))
   if (actualTempFiles.length === 0) {
diff --git __test__/git-command-manager.test.ts __test__/git-command-manager.test.ts
index cea73d4dd..8a97d827a 100644
--- __test__/git-command-manager.test.ts
+++ __test__/git-command-manager.test.ts
@@ -108,7 +108,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     jest.restoreAllMocks()
   })
 
-  it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is true', async () => {
+  it('should call execGit with the correct arguments when fetchDepth is 0', async () => {
     jest.spyOn(exec, 'exec').mockImplementation(mockExec)
     const workingDirectory = 'test'
     const lfs = false
@@ -122,8 +122,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     const refSpec = ['refspec1', 'refspec2']
     const options = {
       filter: 'filterValue',
-      fetchDepth: 0,
-      fetchTags: true
+      fetchDepth: 0
     }
 
     await git.fetch(refSpec, options)
@@ -134,6 +133,7 @@ describe('Test fetchDepth and fetchTags options', () => {
         '-c',
         'protocol.version=2',
         'fetch',
+        '--no-tags',
         '--prune',
         '--no-recurse-submodules',
         '--filter=filterValue',
@@ -145,7 +145,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     )
   })
 
-  it('should call execGit with the correct arguments when fetchDepth is 0 and fetchTags is false', async () => {
+  it('should call execGit with the correct arguments when fetchDepth is 0 and refSpec includes tags', async () => {
     jest.spyOn(exec, 'exec').mockImplementation(mockExec)
 
     const workingDirectory = 'test'
@@ -156,11 +156,10 @@ describe('Test fetchDepth and fetchTags options', () => {
       lfs,
       doSparseCheckout
     )
-    const refSpec = ['refspec1', 'refspec2']
+    const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
     const options = {
       filter: 'filterValue',
-      fetchDepth: 0,
-      fetchTags: false
+      fetchDepth: 0
     }
 
     await git.fetch(refSpec, options)
@@ -177,13 +176,14 @@ describe('Test fetchDepth and fetchTags options', () => {
         '--filter=filterValue',
         'origin',
         'refspec1',
-        'refspec2'
+        'refspec2',
+        '+refs/tags/*:refs/tags/*'
       ],
       expect.any(Object)
     )
   })
 
-  it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is false', async () => {
+  it('should call execGit with the correct arguments when fetchDepth is 1', async () => {
     jest.spyOn(exec, 'exec').mockImplementation(mockExec)
 
     const workingDirectory = 'test'
@@ -197,8 +197,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     const refSpec = ['refspec1', 'refspec2']
     const options = {
       filter: 'filterValue',
-      fetchDepth: 1,
-      fetchTags: false
+      fetchDepth: 1
     }
 
     await git.fetch(refSpec, options)
@@ -222,7 +221,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     )
   })
 
-  it('should call execGit with the correct arguments when fetchDepth is 1 and fetchTags is true', async () => {
+  it('should call execGit with the correct arguments when fetchDepth is 1 and refSpec includes tags', async () => {
     jest.spyOn(exec, 'exec').mockImplementation(mockExec)
 
     const workingDirectory = 'test'
@@ -233,11 +232,10 @@ describe('Test fetchDepth and fetchTags options', () => {
       lfs,
       doSparseCheckout
     )
-    const refSpec = ['refspec1', 'refspec2']
+    const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
     const options = {
       filter: 'filterValue',
-      fetchDepth: 1,
-      fetchTags: true
+      fetchDepth: 1
     }
 
     await git.fetch(refSpec, options)
@@ -248,13 +246,15 @@ describe('Test fetchDepth and fetchTags options', () => {
         '-c',
         'protocol.version=2',
         'fetch',
+        '--no-tags',
         '--prune',
         '--no-recurse-submodules',
         '--filter=filterValue',
         '--depth=1',
         'origin',
         'refspec1',
-        'refspec2'
+        'refspec2',
+        '+refs/tags/*:refs/tags/*'
       ],
       expect.any(Object)
     )
@@ -338,7 +338,7 @@ describe('Test fetchDepth and fetchTags options', () => {
     )
   })
 
-  it('should call execGit with the correct arguments when fetchTags is true and showProgress is true', async () => {
+  it('should call execGit with the correct arguments when showProgress is true and refSpec includes tags', async () => {
     jest.spyOn(exec, 'exec').mockImplementation(mockExec)
 
     const workingDirectory = 'test'
@@ -349,10 +349,9 @@ describe('Test fetchDepth and fetchTags options', () => {
       lfs,
       doSparseCheckout
     )
-    const refSpec = ['refspec1', 'refspec2']
+    const refSpec = ['refspec1', 'refspec2', '+refs/tags/*:refs/tags/*']
     const options = {
       filter: 'filterValue',
-      fetchTags: true,
       showProgress: true
     }
 
@@ -364,15 +363,134 @@ describe('Test fetchDepth and fetchTags options', () => {
         '-c',
         'protocol.version=2',
         'fetch',
+        '--no-tags',
         '--prune',
         '--no-recurse-submodules',
         '--progress',
         '--filter=filterValue',
         'origin',
         'refspec1',
-        'refspec2'
+        'refspec2',
+        '+refs/tags/*:refs/tags/*'
       ],
       expect.any(Object)
     )
   })
 })
+
+describe('git user-agent with orchestration ID', () => {
+  beforeEach(async () => {
+    jest.spyOn(fshelper, 'fileExistsSync').mockImplementation(jest.fn())
+    jest.spyOn(fshelper, 'directoryExistsSync').mockImplementation(jest.fn())
+  })
+
+  afterEach(() => {
+    jest.restoreAllMocks()
+    // Clean up environment variable to prevent test pollution
+    delete process.env['ACTIONS_ORCHESTRATION_ID']
+  })
+
+  it('should include orchestration ID in user-agent when ACTIONS_ORCHESTRATION_ID is set', async () => {
+    const orchId = 'test-orch-id-12345'
+    process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
+
+    let capturedEnv: any = null
+    mockExec.mockImplementation((path, args, options) => {
+      if (args.includes('version')) {
+        options.listeners.stdout(Buffer.from('2.18'))
+      }
+      // Capture env on any command
+      capturedEnv = options.env
+      return 0
+    })
+    jest.spyOn(exec, 'exec').mockImplementation(mockExec)
+
+    const workingDirectory = 'test'
+    const lfs = false
+    const doSparseCheckout = false
+    git = await commandManager.createCommandManager(
+      workingDirectory,
+      lfs,
+      doSparseCheckout
+    )
+
+    // Call a git command to trigger env capture after user-agent is set
+    await git.init()
+
+    // Verify the user agent includes the orchestration ID
+    expect(git).toBeDefined()
+    expect(capturedEnv).toBeDefined()
+    expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
+      `git/2.18 (github-actions-checkout) actions_orchestration_id/${orchId}`
+    )
+  })
+
+  it('should sanitize invalid characters in orchestration ID', async () => {
+    const orchId = 'test (with) special/chars'
+    process.env['ACTIONS_ORCHESTRATION_ID'] = orchId
+
+    let capturedEnv: any = null
+    mockExec.mockImplementation((path, args, options) => {
+      if (args.includes('version')) {
+        options.listeners.stdout(Buffer.from('2.18'))
+      }
+      // Capture env on any command
+      capturedEnv = options.env
+      return 0
+    })
+    jest.spyOn(exec, 'exec').mockImplementation(mockExec)
+
+    const workingDirectory = 'test'
+    const lfs = false
+    const doSparseCheckout = false
+    git = await commandManager.createCommandManager(
+      workingDirectory,
+      lfs,
+      doSparseCheckout
+    )
+
+    // Call a git command to trigger env capture after user-agent is set
+    await git.init()
+
+    // Verify the user agent has sanitized orchestration ID (spaces, parentheses, slash replaced)
+    expect(git).toBeDefined()
+    expect(capturedEnv).toBeDefined()
+    expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
+      'git/2.18 (github-actions-checkout) actions_orchestration_id/test__with__special_chars'
+    )
+  })
+
+  it('should not modify user-agent when ACTIONS_ORCHESTRATION_ID is not set', async () => {
+    delete process.env['ACTIONS_ORCHESTRATION_ID']
+
+    let capturedEnv: any = null
+    mockExec.mockImplementation((path, args, options) => {
+      if (args.includes('version')) {
+        options.listeners.stdout(Buffer.from('2.18'))
+      }
+      // Capture env on any command
+      capturedEnv = options.env
+      return 0
+    })
+    jest.spyOn(exec, 'exec').mockImplementation(mockExec)
+
+    const workingDirectory = 'test'
+    const lfs = false
+    const doSparseCheckout = false
+    git = await commandManager.createCommandManager(
+      workingDirectory,
+      lfs,
+      doSparseCheckout
+    )
+
+    // Call a git command to trigger env capture after user-agent is set
+    await git.init()
+
+    // Verify the user agent does NOT contain orchestration ID
+    expect(git).toBeDefined()
+    expect(capturedEnv).toBeDefined()
+    expect(capturedEnv['GIT_HTTP_USER_AGENT']).toBe(
+      'git/2.18 (github-actions-checkout)'
+    )
+  })
+})
diff --git __test__/git-directory-helper.test.ts __test__/git-directory-helper.test.ts
index 22e9ae6d4..de79dc890 100644
--- __test__/git-directory-helper.test.ts
+++ __test__/git-directory-helper.test.ts
@@ -471,6 +471,7 @@ async function setup(testName: string): Promise<void> {
     configExists: jest.fn(),
     fetch: jest.fn(),
     getDefaultBranch: jest.fn(),
+    getSubmoduleConfigPaths: jest.fn(async () => []),
     getWorkingDirectory: jest.fn(() => repositoryPath),
     init: jest.fn(),
     isDetached: jest.fn(),
@@ -493,12 +494,15 @@ async function setup(testName: string): Promise<void> {
       return true
     }),
     tryConfigUnset: jest.fn(),
+    tryConfigUnsetValue: jest.fn(),
     tryDisableAutomaticGarbageCollection: jest.fn(),
     tryGetFetchUrl: jest.fn(async () => {
       // Sanity check - this function shouldn't be called when the .git directory doesn't exist
       await fs.promises.stat(path.join(repositoryPath, '.git'))
       return repositoryUrl
     }),
+    tryGetConfigValues: jest.fn(),
+    tryGetConfigKeys: jest.fn(),
     tryReset: jest.fn(async () => {
       return true
     }),
diff --git __test__/ref-helper.test.ts __test__/ref-helper.test.ts
index 5c8d76b87..4943abd6d 100644
--- __test__/ref-helper.test.ts
+++ __test__/ref-helper.test.ts
@@ -152,7 +152,22 @@ describe('ref-helper tests', () => {
   it('getRefSpec sha + refs/tags/', async () => {
     const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit)
     expect(refSpec.length).toBe(1)
-    expect(refSpec[0]).toBe(`+${commit}:refs/tags/my-tag`)
+    expect(refSpec[0]).toBe(`+refs/tags/my-tag:refs/tags/my-tag`)
+  })
+
+  it('getRefSpec sha + refs/tags/ with fetchTags', async () => {
+    // When fetchTags is true, only include tags wildcard (specific tag is redundant)
+    const refSpec = refHelper.getRefSpec('refs/tags/my-tag', commit, true)
+    expect(refSpec.length).toBe(1)
+    expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
+  })
+
+  it('getRefSpec sha + refs/heads/ with fetchTags', async () => {
+    // When fetchTags is true, include both the branch refspec and tags wildcard
+    const refSpec = refHelper.getRefSpec('refs/heads/my/branch', commit, true)
+    expect(refSpec.length).toBe(2)
+    expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
+    expect(refSpec[1]).toBe(`+${commit}:refs/remotes/origin/my/branch`)
   })
 
   it('getRefSpec sha only', async () => {
@@ -168,6 +183,14 @@ describe('ref-helper tests', () => {
     expect(refSpec[1]).toBe('+refs/tags/my-ref*:refs/tags/my-ref*')
   })
 
+  it('getRefSpec unqualified ref only with fetchTags', async () => {
+    // When fetchTags is true, skip specific tag pattern since wildcard covers all
+    const refSpec = refHelper.getRefSpec('my-ref', '', true)
+    expect(refSpec.length).toBe(2)
+    expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
+    expect(refSpec[1]).toBe('+refs/heads/my-ref*:refs/remotes/origin/my-ref*')
+  })
+
   it('getRefSpec refs/heads/ only', async () => {
     const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '')
     expect(refSpec.length).toBe(1)
@@ -187,4 +210,21 @@ describe('ref-helper tests', () => {
     expect(refSpec.length).toBe(1)
     expect(refSpec[0]).toBe('+refs/tags/my-tag:refs/tags/my-tag')
   })
+
+  it('getRefSpec refs/tags/ only with fetchTags', async () => {
+    // When fetchTags is true, only include tags wildcard (specific tag is redundant)
+    const refSpec = refHelper.getRefSpec('refs/tags/my-tag', '', true)
+    expect(refSpec.length).toBe(1)
+    expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
+  })
+
+  it('getRefSpec refs/heads/ only with fetchTags', async () => {
+    // When fetchTags is true, include both the branch refspec and tags wildcard
+    const refSpec = refHelper.getRefSpec('refs/heads/my/branch', '', true)
+    expect(refSpec.length).toBe(2)
+    expect(refSpec[0]).toBe('+refs/tags/*:refs/tags/*')
+    expect(refSpec[1]).toBe(
+      '+refs/heads/my/branch:refs/remotes/origin/my/branch'
+    )
+  })
 })
diff --git a/__test__/verify-fetch-tags.sh b/__test__/verify-fetch-tags.sh
new file mode 100755
index 000000000..74cff1ed6
--- /dev/null
+++ __test__/verify-fetch-tags.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+# Verify tags were fetched
+TAG_COUNT=$(git -C ./fetch-tags-test tag | wc -l)
+if [ "$TAG_COUNT" -eq 0 ]; then
+    echo "Expected tags to be fetched, but found none"
+    exit 1
+fi
+echo "Found $TAG_COUNT tags"
diff --git __test__/verify-submodules-recursive.sh __test__/verify-submodules-recursive.sh
index 1b68f9b97..5ecbb42d0 100755
--- __test__/verify-submodules-recursive.sh
+++ __test__/verify-submodules-recursive.sh
@@ -17,7 +17,7 @@ fi
 
 echo "Testing persisted credential"
 pushd ./submodules-recursive/submodule-level-1/submodule-level-2
-git config --local --name-only --get-regexp http.+extraheader && git fetch
+git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
 if [ "$?" != "0" ]; then
     echo "Failed to validate persisted credential"
     popd
diff --git __test__/verify-submodules-true.sh __test__/verify-submodules-true.sh
index 43769fe06..4c311f846 100755
--- __test__/verify-submodules-true.sh
+++ __test__/verify-submodules-true.sh
@@ -17,7 +17,7 @@ fi
 
 echo "Testing persisted credential"
 pushd ./submodules-true/submodule-level-1
-git config --local --name-only --get-regexp http.+extraheader && git fetch
+git config --local --includes --name-only --get-regexp http.+extraheader && git fetch
 if [ "$?" != "0" ]; then
     echo "Failed to validate persisted credential"
     popd
diff --git a/__test__/verify-worktree.sh b/__test__/verify-worktree.sh
new file mode 100755
index 000000000..3a4d3e4df
--- /dev/null
+++ __test__/verify-worktree.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+set -e
+
+# Verify worktree credentials
+# This test verifies that git credentials work in worktrees created after checkout
+# Usage: verify-worktree.sh <checkout-path> <worktree-name>
+
+CHECKOUT_PATH="$1"
+WORKTREE_NAME="$2"
+
+if [ -z "$CHECKOUT_PATH" ] || [ -z "$WORKTREE_NAME" ]; then
+  echo "Usage: verify-worktree.sh <checkout-path> <worktree-name>"
+  exit 1
+fi
+
+cd "$CHECKOUT_PATH"
+
+# Add safe directory for container environments
+git config --global --add safe.directory "*" 2>/dev/null || true
+
+# Show the includeIf configuration
+echo "Git config includeIf entries:"
+git config --list --show-origin | grep -i include || true
+
+# Create the worktree
+echo "Creating worktree..."
+git worktree add "../$WORKTREE_NAME" HEAD --detach
+
+# Change to worktree directory
+cd "../$WORKTREE_NAME"
+
+# Verify we're in a worktree
+echo "Verifying worktree gitdir:"
+cat .git
+
+# Verify credentials are available in worktree by checking extraheader is configured
+echo "Checking credentials in worktree..."
+if git config --list --show-origin | grep -q "extraheader"; then
+  echo "Credentials are configured in worktree"
+else
+  echo "ERROR: Credentials are NOT configured in worktree"
+  echo "Full git config:"
+  git config --list --show-origin
+  exit 1
+fi
+
+# Verify fetch works in the worktree
+echo "Fetching in worktree..."
+git fetch origin
+
+echo "Worktree credentials test passed!"
diff --git action.yml action.yml
index 6842eb843..767c41649 100644
--- action.yml
+++ action.yml
@@ -104,6 +104,6 @@ outputs:
   commit:
     description: 'The commit SHA that was checked out'
 runs:
-  using: node20
+  using: node24
   main: dist/index.js
   post: dist/index.js
diff --git dist/index.js dist/index.js
index f3ae6f3ea..fe3f3170e 100644
--- dist/index.js
+++ dist/index.js
@@ -162,6 +162,7 @@ class GitAuthHelper {
         this.sshKeyPath = '';
         this.sshKnownHostsPath = '';
         this.temporaryHomePath = '';
+        this.credentialsConfigPath = ''; // Path to separate credentials config file in RUNNER_TEMP
         this.git = gitCommandManager;
         this.settings = gitSourceSettings || {};
         // Token auth header
@@ -229,15 +230,17 @@ class GitAuthHelper {
     configureGlobalAuth() {
         return __awaiter(this, void 0, void 0, function* () {
             // 'configureTempGlobalConfig' noops if already set, just returns the path
-            const newGitConfigPath = yield this.configureTempGlobalConfig();
+            yield this.configureTempGlobalConfig();
             try {
                 // Configure the token
-                yield this.configureToken(newGitConfigPath, true);
+                yield this.configureToken(true);
                 // Configure HTTPS instead of SSH
                 yield this.git.tryConfigUnset(this.insteadOfKey, true);
                 if (!this.settings.sshKey) {
                     for (const insteadOfValue of this.insteadOfValues) {
-                        yield this.git.config(this.insteadOfKey, insteadOfValue, true, true);
+                        yield this.git.config(this.insteadOfKey, insteadOfValue, true, // globalConfig?
+                        true // add?
+                        );
                     }
                 }
             }
@@ -252,19 +255,34 @@ class GitAuthHelper {
     configureSubmoduleAuth() {
         return __awaiter(this, void 0, void 0, function* () {
             // Remove possible previous HTTPS instead of SSH
-            yield this.removeGitConfig(this.insteadOfKey, true);
+            yield this.removeSubmoduleGitConfig(this.insteadOfKey);
             if (this.settings.persistCredentials) {
-                // Configure a placeholder value. This approach avoids the credential being captured
-                // by process creation audit events, which are commonly logged. For more information,
-                // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
-                const output = yield this.git.submoduleForeach(
-                // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
-                `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules);
-                // Replace the placeholder
-                const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+                // Get the credentials config file path in RUNNER_TEMP
+                const credentialsConfigPath = this.getCredentialsConfigPath();
+                // Container credentials config path
+                const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
+                // Get submodule config file paths.
+                const configPaths = yield this.git.getSubmoduleConfigPaths(this.settings.nestedSubmodules);
+                // For each submodule, configure includeIf entries pointing to the shared credentials file.
+                // Configure both host and container paths to support Docker container actions.
                 for (const configPath of configPaths) {
-                    core.debug(`Replacing token placeholder in '${configPath}'`);
-                    yield this.replaceTokenPlaceholder(configPath);
+                    // Submodule Git directory
+                    let submoduleGitDir = path.dirname(configPath); // The config file is at .git/modules/submodule-name/config
+                    submoduleGitDir = submoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                    // Configure host includeIf
+                    yield this.git.config(`includeIf.gitdir:${submoduleGitDir}.path`, credentialsConfigPath, false, // globalConfig?
+                    false, // add?
+                    configPath);
+                    // Container submodule git directory
+                    const githubWorkspace = process.env['GITHUB_WORKSPACE'];
+                    assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
+                    let relativeSubmoduleGitDir = path.relative(githubWorkspace, submoduleGitDir);
+                    relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                    const containerSubmoduleGitDir = path.posix.join('/github/workspace', relativeSubmoduleGitDir);
+                    // Configure container includeIf
+                    yield this.git.config(`includeIf.gitdir:${containerSubmoduleGitDir}.path`, containerCredentialsPath, false, // globalConfig?
+                    false, // add?
+                    configPath);
                 }
                 if (this.settings.sshKey) {
                     // Configure core.sshCommand
@@ -295,6 +313,10 @@ class GitAuthHelper {
             }
         });
     }
+    /**
+     * Configures SSH authentication by writing the SSH key and known hosts,
+     * and setting up the GIT_SSH_COMMAND environment variable.
+     */
     configureSsh() {
         return __awaiter(this, void 0, void 0, function* () {
             if (!this.settings.sshKey) {
@@ -351,43 +373,94 @@ class GitAuthHelper {
             }
         });
     }
-    configureToken(configPath, globalConfig) {
-        return __awaiter(this, void 0, void 0, function* () {
-            // Validate args
-            assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations');
-            // Default config path
-            if (!configPath && !globalConfig) {
-                configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config');
-            }
-            // Configure a placeholder value. This approach avoids the credential being captured
-            // by process creation audit events, which are commonly logged. For more information,
-            // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
-            yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig);
-            // Replace the placeholder
-            yield this.replaceTokenPlaceholder(configPath || '');
-        });
-    }
-    replaceTokenPlaceholder(configPath) {
+    /**
+     * Configures token-based authentication by creating a credentials config file
+     * and setting up includeIf entries to reference it.
+     * @param globalConfig Whether to configure global config instead of local
+     */
+    configureToken(globalConfig) {
         return __awaiter(this, void 0, void 0, function* () {
-            assert.ok(configPath, 'configPath is not defined');
-            let content = (yield fs.promises.readFile(configPath)).toString();
+            // Get the credentials config file path in RUNNER_TEMP
+            const credentialsConfigPath = this.getCredentialsConfigPath();
+            // Write placeholder to the separate credentials config file using git config.
+            // This approach avoids the credential being captured by process creation audit events,
+            // which are commonly logged. For more information, refer to
+            // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
+            yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, false, // globalConfig?
+            false, // add?
+            credentialsConfigPath);
+            // Replace the placeholder in the credentials config file
+            let content = (yield fs.promises.readFile(credentialsConfigPath)).toString();
             const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
             if (placeholderIndex < 0 ||
                 placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) {
-                throw new Error(`Unable to replace auth placeholder in ${configPath}`);
+                throw new Error(`Unable to replace auth placeholder in ${credentialsConfigPath}`);
             }
             assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
             content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue);
-            yield fs.promises.writeFile(configPath, content);
+            yield fs.promises.writeFile(credentialsConfigPath, content);
+            // Add include or includeIf to reference the credentials config
+            if (globalConfig) {
+                // Global config file is temporary
+                yield this.git.config('include.path', credentialsConfigPath, true // globalConfig?
+                );
+            }
+            else {
+                // Host git directory
+                let gitDir = path.join(this.git.getWorkingDirectory(), '.git');
+                gitDir = gitDir.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                // Configure host includeIf
+                const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`;
+                yield this.git.config(hostIncludeKey, credentialsConfigPath);
+                // Configure host includeIf for worktrees
+                const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`;
+                yield this.git.config(hostWorktreeIncludeKey, credentialsConfigPath);
+                // Container git directory
+                const workingDirectory = this.git.getWorkingDirectory();
+                const githubWorkspace = process.env['GITHUB_WORKSPACE'];
+                assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined');
+                let relativePath = path.relative(githubWorkspace, workingDirectory);
+                relativePath = relativePath.replace(/\\/g, '/'); // Use forward slashes, even on Windows
+                const containerGitDir = path.posix.join('/github/workspace', relativePath, '.git');
+                // Container credentials config path
+                const containerCredentialsPath = path.posix.join('/github/runner_temp', path.basename(credentialsConfigPath));
+                // Configure container includeIf
+                const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`;
+                yield this.git.config(containerIncludeKey, containerCredentialsPath);
+                // Configure container includeIf for worktrees
+                const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`;
+                yield this.git.config(containerWorktreeIncludeKey, containerCredentialsPath);
+            }
         });
     }
+    /**
+     * Gets or creates the path to the credentials config file in RUNNER_TEMP.
+     * @returns The absolute path to the credentials config file
+     */
+    getCredentialsConfigPath() {
+        if (this.credentialsConfigPath) {
+            return this.credentialsConfigPath;
+        }
+        const runnerTemp = process.env['RUNNER_TEMP'] || '';
+        assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
+        // Create a unique filename for this checkout instance
+        const configFileName = `git-credentials-${(0, uuid_1.v4)()}.config`;
+        this.credentialsConfigPath = path.join(runnerTemp, configFileName);
+        core.debug(`Credentials config path: ${this.credentialsConfigPath}`);
+        return this.credentialsConfigPath;
+    }
+    /**
+     * Removes SSH authentication configuration by cleaning up SSH keys,
+     * known hosts files, and SSH command configurations.
+     */
     removeSsh() {
         return __awaiter(this, void 0, void 0, function* () {
-            var _a;
+            var _a, _b;
             // SSH key
             const keyPath = this.sshKeyPath || stateHelper.SshKeyPath;
             if (keyPath) {
                 try {
+                    core.info(`Removing SSH key '${keyPath}'`);
                     yield io.rmRF(keyPath);
                 }
                 catch (err) {
@@ -399,37 +472,136 @@ class GitAuthHelper {
             const knownHostsPath = this.sshKnownHostsPath || stateHelper.SshKnownHostsPath;
             if (knownHostsPath) {
                 try {
+                    core.info(`Removing SSH known hosts '${knownHostsPath}'`);
                     yield io.rmRF(knownHostsPath);
                 }
-                catch (_b) {
-                    // Intentionally empty
+                catch (err) {
+                    core.debug(`${(_b = err === null || err === void 0 ? void 0 : err.message) !== null && _b !== void 0 ? _b : err}`);
+                    core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`);
                 }
             }
             // SSH command
+            core.info('Removing SSH command configuration');
             yield this.removeGitConfig(SSH_COMMAND_KEY);
+            yield this.removeSubmoduleGitConfig(SSH_COMMAND_KEY);
         });
     }
+    /**
+     * Removes token-based authentication by cleaning up HTTP headers,
+     * includeIf entries, and credentials config files.
+     */
     removeToken() {
         return __awaiter(this, void 0, void 0, function* () {
-            // HTTP extra header
+            var _a;
+            // Remove HTTP extra header
+            core.info('Removing HTTP extra header');
             yield this.removeGitConfig(this.tokenConfigKey);
+            yield this.removeSubmoduleGitConfig(this.tokenConfigKey);
+            // Collect credentials config paths that need to be removed
+            const credentialsPaths = new Set();
+            // Remove includeIf entries that point to git-credentials-*.config files
+            core.info('Removing includeIf entries pointing to credentials config files');
+            const mainCredentialsPaths = yield this.removeIncludeIfCredentials();
+            mainCredentialsPaths.forEach(path => credentialsPaths.add(path));
+            // Remove submodule includeIf entries that point to git-credentials-*.config files
+            const submoduleConfigPaths = yield this.git.getSubmoduleConfigPaths(true);
+            for (const configPath of submoduleConfigPaths) {
+                const submoduleCredentialsPaths = yield this.removeIncludeIfCredentials(configPath);
+                submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path));
+            }
+            // Remove credentials config files
+            for (const credentialsPath of credentialsPaths) {
+                // Only remove credentials config files if they are under RUNNER_TEMP
+                const runnerTemp = process.env['RUNNER_TEMP'];
+                assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
+                if (credentialsPath.startsWith(runnerTemp)) {
+                    try {
+                        core.info(`Removing credentials config '${credentialsPath}'`);
+                        yield io.rmRF(credentialsPath);
+                    }
+                    catch (err) {
+                        core.debug(`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
+                        core.warning(`Failed to remove credentials config '${credentialsPath}'`);
+                    }
+                }
+                else {
+                    core.debug(`Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`);
+                }
+            }
         });
     }
-    removeGitConfig(configKey_1) {
-        return __awaiter(this, arguments, void 0, function* (configKey, submoduleOnly = false) {
-            if (!submoduleOnly) {
-                if ((yield this.git.configExists(configKey)) &&
-                    !(yield this.git.tryConfigUnset(configKey))) {
-                    // Load the config contents
-                    core.warning(`Failed to remove '${configKey}' from the git config`);
-                }
+    /**
+     * Removes a git config key from the local repository config.
+     * @param configKey The git config key to remove
+     */
+    removeGitConfig(configKey) {
+        return __awaiter(this, void 0, void 0, function* () {
+            if ((yield this.git.configExists(configKey)) &&
+                !(yield this.git.tryConfigUnset(configKey))) {
+                // Load the config contents
+                core.warning(`Failed to remove '${configKey}' from the git config`);
             }
+        });
+    }
+    /**
+     * Removes a git config key from all submodule configs.
+     * @param configKey The git config key to remove
+     */
+    removeSubmoduleGitConfig(configKey) {
+        return __awaiter(this, void 0, void 0, function* () {
             const pattern = regexpHelper.escape(configKey);
             yield this.git.submoduleForeach(
-            // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
+            // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
             `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`, true);
         });
     }
+    /**
+     * Removes includeIf entries that point to git-credentials-*.config files.
+     * @param configPath Optional path to a specific git config file to operate on
+     * @returns Array of unique credentials config file paths that were found and removed
+     */
+    removeIncludeIfCredentials(configPath) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const credentialsPaths = new Set();
+            try {
+                // Get all includeIf.gitdir keys
+                const keys = yield this.git.tryGetConfigKeys('^includeIf\\.gitdir:', false, // globalConfig?
+                configPath);
+                for (const key of keys) {
+                    // Get all values for this key
+                    const values = yield this.git.tryGetConfigValues(key, false, // globalConfig?
+                    configPath);
+                    if (values.length > 0) {
+                        // Remove only values that match git-credentials-<uuid>.config pattern
+                        for (const value of values) {
+                            if (this.testCredentialsConfigPath(value)) {
+                                credentialsPaths.add(value);
+                                yield this.git.tryConfigUnsetValue(key, value, false, configPath);
+                            }
+                        }
+                    }
+                }
+            }
+            catch (err) {
+                // Ignore errors - this is cleanup code
+                if (configPath) {
+                    core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`);
+                }
+                else {
+                    core.debug(`Error during includeIf cleanup: ${err}`);
+                }
+            }
+            return Array.from(credentialsPaths);
+        });
+    }
+    /**
+     * Tests if a path matches the git-credentials-*.config pattern.
+     * @param path The path to test
+     * @returns True if the path matches the credentials config pattern
+     */
+    testCredentialsConfigPath(path) {
+        return /git-credentials-[0-9a-f-]+\.config$/i.test(path);
+    }
 }
 
 
@@ -481,7 +653,6 @@ const fs = __importStar(__nccwpck_require__(7147));
 const fshelper = __importStar(__nccwpck_require__(7219));
 const io = __importStar(__nccwpck_require__(7436));
 const path = __importStar(__nccwpck_require__(1017));
-const refHelper = __importStar(__nccwpck_require__(8601));
 const regexpHelper = __importStar(__nccwpck_require__(3120));
 const retryHelper = __importStar(__nccwpck_require__(2155));
 const git_version_1 = __nccwpck_require__(3142);
@@ -627,9 +798,15 @@ class GitCommandManager {
             yield this.execGit(args);
         });
     }
-    config(configKey, configValue, globalConfig, add) {
+    config(configKey, configValue, globalConfig, add, configFile) {
         return __awaiter(this, void 0, void 0, function* () {
-            const args = ['config', globalConfig ? '--global' : '--local'];
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
             if (add) {
                 args.push('--add');
             }
@@ -653,9 +830,9 @@ class GitCommandManager {
     fetch(refSpec, options) {
         return __awaiter(this, void 0, void 0, function* () {
             const args = ['-c', 'protocol.version=2', 'fetch'];
-            if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
-                args.push('--no-tags');
-            }
+            // Always use --no-tags for explicit control over tag fetching
+            // Tags are fetched explicitly via refspec when needed
+            args.push('--no-tags');
             args.push('--prune', '--no-recurse-submodules');
             if (options.showProgress) {
                 args.push('--progress');
@@ -706,6 +883,16 @@ class GitCommandManager {
             throw new Error('Unexpected output when retrieving default branch');
         });
     }
+    getSubmoduleConfigPaths(recursive) {
+        return __awaiter(this, void 0, void 0, function* () {
+            // Get submodule config file paths.
+            // Use `--show-origin` to get the config file path for each submodule.
+            const output = yield this.submoduleForeach(`git config --local --show-origin --name-only --get-regexp remote.origin.url`, recursive);
+            // Extract config file paths from the output (lines starting with "file:").
+            const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
+            return configPaths;
+        });
+    }
     getWorkingDirectory() {
         return this.workingDirectory;
     }
@@ -836,6 +1023,20 @@ class GitCommandManager {
             return output.exitCode === 0;
         });
     }
+    tryConfigUnsetValue(configKey, configValue, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--unset', configKey, configValue);
+            const output = yield this.execGit(args, true);
+            return output.exitCode === 0;
+        });
+    }
     tryDisableAutomaticGarbageCollection() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['config', '--local', 'gc.auto', '0'], true);
@@ -855,6 +1056,46 @@ class GitCommandManager {
             return stdout;
         });
     }
+    tryGetConfigValues(configKey, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--get-all', configKey);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(value => value.trim());
+        });
+    }
+    tryGetConfigKeys(pattern, globalConfig, configFile) {
+        return __awaiter(this, void 0, void 0, function* () {
+            const args = ['config'];
+            if (configFile) {
+                args.push('--file', configFile);
+            }
+            else {
+                args.push(globalConfig ? '--global' : '--local');
+            }
+            args.push('--name-only', '--get-regexp', pattern);
+            const output = yield this.execGit(args, true);
+            if (output.exitCode !== 0) {
+                return [];
+            }
+            return output.stdout
+                .trim()
+                .split('\n')
+                .filter(key => key.trim());
+        });
+    }
     tryReset() {
         return __awaiter(this, void 0, void 0, function* () {
             const output = yield this.execGit(['reset', '--hard', 'HEAD'], true);
@@ -964,7 +1205,17 @@ class GitCommandManager {
                 }
             }
             // Set the user agent
-            const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`;
+            let gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`;
+            // Append orchestration ID if set
+            const orchId = process.env['ACTIONS_ORCHESTRATION_ID'];
+            if (orchId) {
+                // Sanitize the orchestration ID to ensure it contains only valid characters
+                // Valid characters: 0-9, a-z, _, -, .
+                const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_');
+                if (sanitizedId) {
+                    gitHttpUserAgent = `${gitHttpUserAgent} actions_orchestration_id/${sanitizedId}`;
+                }
+            }
             core.debug(`Set git useragent to: ${gitHttpUserAgent}`);
             this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent;
         });
@@ -1287,13 +1538,26 @@ function getSource(settings) {
                 if (!(yield refHelper.testRef(git, settings.ref, settings.commit))) {
                     refSpec = refHelper.getRefSpec(settings.ref, settings.commit);
                     yield git.fetch(refSpec, fetchOptions);
+                    // Verify the ref now matches. For branches, the targeted fetch above brings
+                    // in the specific commit. For tags (fetched by ref), this will fail if
+                    // the tag was moved after the workflow was triggered.
+                    if (!(yield refHelper.testRef(git, settings.ref, settings.commit))) {
+                        throw new Error(`The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
+                            `The ref may have been updated after the workflow was triggered.`);
+                    }
                 }
             }
             else {
                 fetchOptions.fetchDepth = settings.fetchDepth;
-                fetchOptions.fetchTags = settings.fetchTags;
-                const refSpec = refHelper.getRefSpec(settings.ref, settings.commit);
+                const refSpec = refHelper.getRefSpec(settings.ref, settings.commit, settings.fetchTags);
                 yield git.fetch(refSpec, fetchOptions);
+                // For tags, verify the ref still points to the expected commit.
+                // Tags are fetched by ref (not commit), so if a tag was moved after the
+                // workflow was triggered, we would silently check out the wrong commit.
+                if (!(yield refHelper.testRef(git, settings.ref, settings.commit))) {
+                    throw new Error(`The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
+                        `The ref may have been updated after the workflow was triggered.`);
+                }
             }
             core.endGroup();
             // Checkout info
@@ -2032,53 +2296,67 @@ function getRefSpecForAllHistory(ref, commit) {
     }
     return result;
 }
-function getRefSpec(ref, commit) {
+function getRefSpec(ref, commit, fetchTags) {
     if (!ref && !commit) {
         throw new Error('Args ref and commit cannot both be empty');
     }
     const upperRef = (ref || '').toUpperCase();
+    const result = [];
+    // When fetchTags is true, always include the tags refspec
+    if (fetchTags) {
+        result.push(exports.tagsRefSpec);
+    }
     // SHA
     if (commit) {
         // refs/heads
         if (upperRef.startsWith('REFS/HEADS/')) {
             const branch = ref.substring('refs/heads/'.length);
-            return [`+${commit}:refs/remotes/origin/${branch}`];
+            result.push(`+${commit}:refs/remotes/origin/${branch}`);
         }
         // refs/pull/
         else if (upperRef.startsWith('REFS/PULL/')) {
             const branch = ref.substring('refs/pull/'.length);
-            return [`+${commit}:refs/remotes/pull/${branch}`];
+            result.push(`+${commit}:refs/remotes/pull/${branch}`);
         }
         // refs/tags/
         else if (upperRef.startsWith('REFS/TAGS/')) {
-            return [`+${commit}:${ref}`];
+            if (!fetchTags) {
+                result.push(`+${ref}:${ref}`);
+            }
         }
         // Otherwise no destination ref
         else {
-            return [commit];
+            result.push(commit);
         }
     }
     // Unqualified ref, check for a matching branch or tag
     else if (!upperRef.startsWith('REFS/')) {
-        return [
-            `+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
-            `+refs/tags/${ref}*:refs/tags/${ref}*`
-        ];
+        result.push(`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`);
+        if (!fetchTags) {
+            result.push(`+refs/tags/${ref}*:refs/tags/${ref}*`);
+        }
     }
     // refs/heads/
     else if (upperRef.startsWith('REFS/HEADS/')) {
         const branch = ref.substring('refs/heads/'.length);
-        return [`+${ref}:refs/remotes/origin/${branch}`];
+        result.push(`+${ref}:refs/remotes/origin/${branch}`);
     }
     // refs/pull/
     else if (upperRef.startsWith('REFS/PULL/')) {
         const branch = ref.substring('refs/pull/'.length);
-        return [`+${ref}:refs/remotes/pull/${branch}`];
+        result.push(`+${ref}:refs/remotes/pull/${branch}`);
     }
     // refs/tags/
+    else if (upperRef.startsWith('REFS/TAGS/')) {
+        if (!fetchTags) {
+            result.push(`+${ref}:${ref}`);
+        }
+    }
+    // Other refs
     else {
-        return [`+${ref}:${ref}`];
+        result.push(`+${ref}:${ref}`);
     }
+    return result;
 }
 /**
  * Tests whether the initial fetch created the ref at the expected commit
@@ -2114,7 +2392,9 @@ function testRef(git, ref, commit) {
         // refs/tags/
         else if (upperRef.startsWith('REFS/TAGS/')) {
             const tagName = ref.substring('refs/tags/'.length);
-            return ((yield git.tagExists(tagName)) && commit === (yield git.revParse(ref)));
+            // Use ^{commit} to dereference annotated tags to their underlying commit
+            return ((yield git.tagExists(tagName)) &&
+                commit === (yield git.revParse(`${ref}^{commit}`)));
         }
         // Unexpected
         else {
diff --git package.json package.json
index dbbaabbae..4b2b58a51 100644
--- package.json
+++ package.json
@@ -1,6 +1,6 @@
 {
   "name": "checkout",
-  "version": "4.3.0",
+  "version": "5.0.0",
   "description": "checkout action",
   "main": "lib/main.js",
   "scripts": {
@@ -37,7 +37,7 @@
   },
   "devDependencies": {
     "@types/jest": "^29.5.12",
-    "@types/node": "^20.12.12",
+    "@types/node": "^24.1.0",
     "@types/uuid": "^9.0.8",
     "@typescript-eslint/eslint-plugin": "^7.9.0",
     "@typescript-eslint/parser": "^7.9.0",
diff --git src/git-auth-helper.ts src/git-auth-helper.ts
index 126e8e5ee..e67db148a 100644
--- src/git-auth-helper.ts
+++ src/git-auth-helper.ts
@@ -43,6 +43,7 @@ class GitAuthHelper {
   private sshKeyPath = ''
   private sshKnownHostsPath = ''
   private temporaryHomePath = ''
+  private credentialsConfigPath = '' // Path to separate credentials config file in RUNNER_TEMP
 
   constructor(
     gitCommandManager: IGitCommandManager,
@@ -126,16 +127,21 @@ class GitAuthHelper {
 
   async configureGlobalAuth(): Promise<void> {
     // 'configureTempGlobalConfig' noops if already set, just returns the path
-    const newGitConfigPath = await this.configureTempGlobalConfig()
+    await this.configureTempGlobalConfig()
     try {
       // Configure the token
-      await this.configureToken(newGitConfigPath, true)
+      await this.configureToken(true)
 
       // Configure HTTPS instead of SSH
       await this.git.tryConfigUnset(this.insteadOfKey, true)
       if (!this.settings.sshKey) {
         for (const insteadOfValue of this.insteadOfValues) {
-          await this.git.config(this.insteadOfKey, insteadOfValue, true, true)
+          await this.git.config(
+            this.insteadOfKey,
+            insteadOfValue,
+            true, // globalConfig?
+            true // add?
+          )
         }
       }
     } catch (err) {
@@ -150,24 +156,60 @@ class GitAuthHelper {
 
   async configureSubmoduleAuth(): Promise<void> {
     // Remove possible previous HTTPS instead of SSH
-    await this.removeGitConfig(this.insteadOfKey, true)
+    await this.removeSubmoduleGitConfig(this.insteadOfKey)
 
     if (this.settings.persistCredentials) {
-      // Configure a placeholder value. This approach avoids the credential being captured
-      // by process creation audit events, which are commonly logged. For more information,
-      // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
-      const output = await this.git.submoduleForeach(
-        // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
-        `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`,
+      // Get the credentials config file path in RUNNER_TEMP
+      const credentialsConfigPath = this.getCredentialsConfigPath()
+
+      // Container credentials config path
+      const containerCredentialsPath = path.posix.join(
+        '/github/runner_temp',
+        path.basename(credentialsConfigPath)
+      )
+
+      // Get submodule config file paths.
+      const configPaths = await this.git.getSubmoduleConfigPaths(
         this.settings.nestedSubmodules
       )
 
-      // Replace the placeholder
-      const configPaths: string[] =
-        output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+      // For each submodule, configure includeIf entries pointing to the shared credentials file.
+      // Configure both host and container paths to support Docker container actions.
       for (const configPath of configPaths) {
-        core.debug(`Replacing token placeholder in '${configPath}'`)
-        await this.replaceTokenPlaceholder(configPath)
+        // Submodule Git directory
+        let submoduleGitDir = path.dirname(configPath) // The config file is at .git/modules/submodule-name/config
+        submoduleGitDir = submoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+
+        // Configure host includeIf
+        await this.git.config(
+          `includeIf.gitdir:${submoduleGitDir}.path`,
+          credentialsConfigPath,
+          false, // globalConfig?
+          false, // add?
+          configPath
+        )
+
+        // Container submodule git directory
+        const githubWorkspace = process.env['GITHUB_WORKSPACE']
+        assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
+        let relativeSubmoduleGitDir = path.relative(
+          githubWorkspace,
+          submoduleGitDir
+        )
+        relativeSubmoduleGitDir = relativeSubmoduleGitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+        const containerSubmoduleGitDir = path.posix.join(
+          '/github/workspace',
+          relativeSubmoduleGitDir
+        )
+
+        // Configure container includeIf
+        await this.git.config(
+          `includeIf.gitdir:${containerSubmoduleGitDir}.path`,
+          containerCredentialsPath,
+          false, // globalConfig?
+          false, // add?
+          configPath
+        )
       }
 
       if (this.settings.sshKey) {
@@ -201,6 +243,10 @@ class GitAuthHelper {
     }
   }
 
+  /**
+   * Configures SSH authentication by writing the SSH key and known hosts,
+   * and setting up the GIT_SSH_COMMAND environment variable.
+   */
   private async configureSsh(): Promise<void> {
     if (!this.settings.sshKey) {
       return
@@ -272,57 +318,127 @@ class GitAuthHelper {
     }
   }
 
-  private async configureToken(
-    configPath?: string,
-    globalConfig?: boolean
-  ): Promise<void> {
-    // Validate args
-    assert.ok(
-      (configPath && globalConfig) || (!configPath && !globalConfig),
-      'Unexpected configureToken parameter combinations'
-    )
-
-    // Default config path
-    if (!configPath && !globalConfig) {
-      configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config')
-    }
-
-    // Configure a placeholder value. This approach avoids the credential being captured
-    // by process creation audit events, which are commonly logged. For more information,
-    // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
+  /**
+   * Configures token-based authentication by creating a credentials config file
+   * and setting up includeIf entries to reference it.
+   * @param globalConfig Whether to configure global config instead of local
+   */
+  private async configureToken(globalConfig?: boolean): Promise<void> {
+    // Get the credentials config file path in RUNNER_TEMP
+    const credentialsConfigPath = this.getCredentialsConfigPath()
+
+    // Write placeholder to the separate credentials config file using git config.
+    // This approach avoids the credential being captured by process creation audit events,
+    // which are commonly logged. For more information, refer to
+    // https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing
     await this.git.config(
       this.tokenConfigKey,
       this.tokenPlaceholderConfigValue,
-      globalConfig
+      false, // globalConfig?
+      false, // add?
+      credentialsConfigPath
     )
 
-    // Replace the placeholder
-    await this.replaceTokenPlaceholder(configPath || '')
-  }
-
-  private async replaceTokenPlaceholder(configPath: string): Promise<void> {
-    assert.ok(configPath, 'configPath is not defined')
-    let content = (await fs.promises.readFile(configPath)).toString()
+    // Replace the placeholder in the credentials config file
+    let content = (await fs.promises.readFile(credentialsConfigPath)).toString()
     const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
     if (
       placeholderIndex < 0 ||
       placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
     ) {
-      throw new Error(`Unable to replace auth placeholder in ${configPath}`)
+      throw new Error(
+        `Unable to replace auth placeholder in ${credentialsConfigPath}`
+      )
     }
     assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
     content = content.replace(
       this.tokenPlaceholderConfigValue,
       this.tokenConfigValue
     )
-    await fs.promises.writeFile(configPath, content)
+    await fs.promises.writeFile(credentialsConfigPath, content)
+
+    // Add include or includeIf to reference the credentials config
+    if (globalConfig) {
+      // Global config file is temporary
+      await this.git.config(
+        'include.path',
+        credentialsConfigPath,
+        true // globalConfig?
+      )
+    } else {
+      // Host git directory
+      let gitDir = path.join(this.git.getWorkingDirectory(), '.git')
+      gitDir = gitDir.replace(/\\/g, '/') // Use forward slashes, even on Windows
+
+      // Configure host includeIf
+      const hostIncludeKey = `includeIf.gitdir:${gitDir}.path`
+      await this.git.config(hostIncludeKey, credentialsConfigPath)
+
+      // Configure host includeIf for worktrees
+      const hostWorktreeIncludeKey = `includeIf.gitdir:${gitDir}/worktrees/*.path`
+      await this.git.config(hostWorktreeIncludeKey, credentialsConfigPath)
+
+      // Container git directory
+      const workingDirectory = this.git.getWorkingDirectory()
+      const githubWorkspace = process.env['GITHUB_WORKSPACE']
+      assert.ok(githubWorkspace, 'GITHUB_WORKSPACE is not defined')
+      let relativePath = path.relative(githubWorkspace, workingDirectory)
+      relativePath = relativePath.replace(/\\/g, '/') // Use forward slashes, even on Windows
+      const containerGitDir = path.posix.join(
+        '/github/workspace',
+        relativePath,
+        '.git'
+      )
+
+      // Container credentials config path
+      const containerCredentialsPath = path.posix.join(
+        '/github/runner_temp',
+        path.basename(credentialsConfigPath)
+      )
+
+      // Configure container includeIf
+      const containerIncludeKey = `includeIf.gitdir:${containerGitDir}.path`
+      await this.git.config(containerIncludeKey, containerCredentialsPath)
+
+      // Configure container includeIf for worktrees
+      const containerWorktreeIncludeKey = `includeIf.gitdir:${containerGitDir}/worktrees/*.path`
+      await this.git.config(
+        containerWorktreeIncludeKey,
+        containerCredentialsPath
+      )
+    }
+  }
+
+  /**
+   * Gets or creates the path to the credentials config file in RUNNER_TEMP.
+   * @returns The absolute path to the credentials config file
+   */
+  private getCredentialsConfigPath(): string {
+    if (this.credentialsConfigPath) {
+      return this.credentialsConfigPath
+    }
+
+    const runnerTemp = process.env['RUNNER_TEMP'] || ''
+    assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
+
+    // Create a unique filename for this checkout instance
+    const configFileName = `git-credentials-${uuid()}.config`
+    this.credentialsConfigPath = path.join(runnerTemp, configFileName)
+
+    core.debug(`Credentials config path: ${this.credentialsConfigPath}`)
+    return this.credentialsConfigPath
   }
 
+  /**
+   * Removes SSH authentication configuration by cleaning up SSH keys,
+   * known hosts files, and SSH command configurations.
+   */
   private async removeSsh(): Promise<void> {
     // SSH key
     const keyPath = this.sshKeyPath || stateHelper.SshKeyPath
     if (keyPath) {
       try {
+        core.info(`Removing SSH key '${keyPath}'`)
         await io.rmRF(keyPath)
       } catch (err) {
         core.debug(`${(err as any)?.message ?? err}`)
@@ -335,40 +451,149 @@ class GitAuthHelper {
       this.sshKnownHostsPath || stateHelper.SshKnownHostsPath
     if (knownHostsPath) {
       try {
+        core.info(`Removing SSH known hosts '${knownHostsPath}'`)
         await io.rmRF(knownHostsPath)
-      } catch {
-        // Intentionally empty
+      } catch (err) {
+        core.debug(`${(err as any)?.message ?? err}`)
+        core.warning(`Failed to remove SSH known hosts '${knownHostsPath}'`)
       }
     }
 
     // SSH command
+    core.info('Removing SSH command configuration')
     await this.removeGitConfig(SSH_COMMAND_KEY)
+    await this.removeSubmoduleGitConfig(SSH_COMMAND_KEY)
   }
 
+  /**
+   * Removes token-based authentication by cleaning up HTTP headers,
+   * includeIf entries, and credentials config files.
+   */
   private async removeToken(): Promise<void> {
-    // HTTP extra header
+    // Remove HTTP extra header
+    core.info('Removing HTTP extra header')
     await this.removeGitConfig(this.tokenConfigKey)
-  }
+    await this.removeSubmoduleGitConfig(this.tokenConfigKey)
+
+    // Collect credentials config paths that need to be removed
+    const credentialsPaths = new Set<string>()
+
+    // Remove includeIf entries that point to git-credentials-*.config files
+    core.info('Removing includeIf entries pointing to credentials config files')
+    const mainCredentialsPaths = await this.removeIncludeIfCredentials()
+    mainCredentialsPaths.forEach(path => credentialsPaths.add(path))
+
+    // Remove submodule includeIf entries that point to git-credentials-*.config files
+    const submoduleConfigPaths = await this.git.getSubmoduleConfigPaths(true)
+    for (const configPath of submoduleConfigPaths) {
+      const submoduleCredentialsPaths =
+        await this.removeIncludeIfCredentials(configPath)
+      submoduleCredentialsPaths.forEach(path => credentialsPaths.add(path))
+    }
 
-  private async removeGitConfig(
-    configKey: string,
-    submoduleOnly: boolean = false
-  ): Promise<void> {
-    if (!submoduleOnly) {
-      if (
-        (await this.git.configExists(configKey)) &&
-        !(await this.git.tryConfigUnset(configKey))
-      ) {
-        // Load the config contents
-        core.warning(`Failed to remove '${configKey}' from the git config`)
+    // Remove credentials config files
+    for (const credentialsPath of credentialsPaths) {
+      // Only remove credentials config files if they are under RUNNER_TEMP
+      const runnerTemp = process.env['RUNNER_TEMP']
+      assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
+      if (credentialsPath.startsWith(runnerTemp)) {
+        try {
+          core.info(`Removing credentials config '${credentialsPath}'`)
+          await io.rmRF(credentialsPath)
+        } catch (err) {
+          core.debug(`${(err as any)?.message ?? err}`)
+          core.warning(
+            `Failed to remove credentials config '${credentialsPath}'`
+          )
+        }
+      } else {
+        core.debug(
+          `Skipping removal of credentials config '${credentialsPath}' - not under RUNNER_TEMP`
+        )
       }
     }
+  }
+
+  /**
+   * Removes a git config key from the local repository config.
+   * @param configKey The git config key to remove
+   */
+  private async removeGitConfig(configKey: string): Promise<void> {
+    if (
+      (await this.git.configExists(configKey)) &&
+      !(await this.git.tryConfigUnset(configKey))
+    ) {
+      // Load the config contents
+      core.warning(`Failed to remove '${configKey}' from the git config`)
+    }
+  }
 
+  /**
+   * Removes a git config key from all submodule configs.
+   * @param configKey The git config key to remove
+   */
+  private async removeSubmoduleGitConfig(configKey: string): Promise<void> {
     const pattern = regexpHelper.escape(configKey)
     await this.git.submoduleForeach(
-      // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline
+      // Wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline.
       `sh -c "git config --local --name-only --get-regexp '${pattern}' && git config --local --unset-all '${configKey}' || :"`,
       true
     )
   }
+
+  /**
+   * Removes includeIf entries that point to git-credentials-*.config files.
+   * @param configPath Optional path to a specific git config file to operate on
+   * @returns Array of unique credentials config file paths that were found and removed
+   */
+  private async removeIncludeIfCredentials(
+    configPath?: string
+  ): Promise<string[]> {
+    const credentialsPaths = new Set<string>()
+
+    try {
+      // Get all includeIf.gitdir keys
+      const keys = await this.git.tryGetConfigKeys(
+        '^includeIf\\.gitdir:',
+        false, // globalConfig?
+        configPath
+      )
+
+      for (const key of keys) {
+        // Get all values for this key
+        const values = await this.git.tryGetConfigValues(
+          key,
+          false, // globalConfig?
+          configPath
+        )
+        if (values.length > 0) {
+          // Remove only values that match git-credentials-<uuid>.config pattern
+          for (const value of values) {
+            if (this.testCredentialsConfigPath(value)) {
+              credentialsPaths.add(value)
+              await this.git.tryConfigUnsetValue(key, value, false, configPath)
+            }
+          }
+        }
+      }
+    } catch (err) {
+      // Ignore errors - this is cleanup code
+      if (configPath) {
+        core.debug(`Error during includeIf cleanup for ${configPath}: ${err}`)
+      } else {
+        core.debug(`Error during includeIf cleanup: ${err}`)
+      }
+    }
+
+    return Array.from(credentialsPaths)
+  }
+
+  /**
+   * Tests if a path matches the git-credentials-*.config pattern.
+   * @param path The path to test
+   * @returns True if the path matches the credentials config pattern
+   */
+  private testCredentialsConfigPath(path: string): boolean {
+    return /git-credentials-[0-9a-f-]+\.config$/i.test(path)
+  }
 }
diff --git src/git-command-manager.ts src/git-command-manager.ts
index 8e42a387f..f5ba40e9f 100644
--- src/git-command-manager.ts
+++ src/git-command-manager.ts
@@ -28,7 +28,8 @@ export interface IGitCommandManager {
     configKey: string,
     configValue: string,
     globalConfig?: boolean,
-    add?: boolean
+    add?: boolean,
+    configFile?: string
   ): Promise<void>
   configExists(configKey: string, globalConfig?: boolean): Promise<boolean>
   fetch(
@@ -36,11 +37,11 @@ export interface IGitCommandManager {
     options: {
       filter?: string
       fetchDepth?: number
-      fetchTags?: boolean
       showProgress?: boolean
     }
   ): Promise<void>
   getDefaultBranch(repositoryUrl: string): Promise<string>
+  getSubmoduleConfigPaths(recursive: boolean): Promise<string[]>
   getWorkingDirectory(): string
   init(): Promise<void>
   isDetached(): Promise<boolean>
@@ -59,8 +60,24 @@ export interface IGitCommandManager {
   tagExists(pattern: string): Promise<boolean>
   tryClean(): Promise<boolean>
   tryConfigUnset(configKey: string, globalConfig?: boolean): Promise<boolean>
+  tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean>
   tryDisableAutomaticGarbageCollection(): Promise<boolean>
   tryGetFetchUrl(): Promise<string>
+  tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
+  tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]>
   tryReset(): Promise<boolean>
   version(): Promise<GitVersion>
 }
@@ -223,9 +240,15 @@ class GitCommandManager {
     configKey: string,
     configValue: string,
     globalConfig?: boolean,
-    add?: boolean
+    add?: boolean,
+    configFile?: string
   ): Promise<void> {
-    const args: string[] = ['config', globalConfig ? '--global' : '--local']
+    const args: string[] = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
     if (add) {
       args.push('--add')
     }
@@ -256,14 +279,13 @@ class GitCommandManager {
     options: {
       filter?: string
       fetchDepth?: number
-      fetchTags?: boolean
       showProgress?: boolean
     }
   ): Promise<void> {
     const args = ['-c', 'protocol.version=2', 'fetch']
-    if (!refSpec.some(x => x === refHelper.tagsRefSpec) && !options.fetchTags) {
-      args.push('--no-tags')
-    }
+    // Always use --no-tags for explicit control over tag fetching
+    // Tags are fetched explicitly via refspec when needed
+    args.push('--no-tags')
 
     args.push('--prune', '--no-recurse-submodules')
     if (options.showProgress) {
@@ -323,6 +345,21 @@ class GitCommandManager {
     throw new Error('Unexpected output when retrieving default branch')
   }
 
+  async getSubmoduleConfigPaths(recursive: boolean): Promise<string[]> {
+    // Get submodule config file paths.
+    // Use `--show-origin` to get the config file path for each submodule.
+    const output = await this.submoduleForeach(
+      `git config --local --show-origin --name-only --get-regexp remote.origin.url`,
+      recursive
+    )
+
+    // Extract config file paths from the output (lines starting with "file:").
+    const configPaths =
+      output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []
+
+    return configPaths
+  }
+
   getWorkingDirectory(): string {
     return this.workingDirectory
   }
@@ -455,6 +492,24 @@ class GitCommandManager {
     return output.exitCode === 0
   }
 
+  async tryConfigUnsetValue(
+    configKey: string,
+    configValue: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<boolean> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--unset', configKey, configValue)
+
+    const output = await this.execGit(args, true)
+    return output.exitCode === 0
+  }
+
   async tryDisableAutomaticGarbageCollection(): Promise<boolean> {
     const output = await this.execGit(
       ['config', '--local', 'gc.auto', '0'],
@@ -481,6 +536,56 @@ class GitCommandManager {
     return stdout
   }
 
+  async tryGetConfigValues(
+    configKey: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--get-all', configKey)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(value => value.trim())
+  }
+
+  async tryGetConfigKeys(
+    pattern: string,
+    globalConfig?: boolean,
+    configFile?: string
+  ): Promise<string[]> {
+    const args = ['config']
+    if (configFile) {
+      args.push('--file', configFile)
+    } else {
+      args.push(globalConfig ? '--global' : '--local')
+    }
+    args.push('--name-only', '--get-regexp', pattern)
+
+    const output = await this.execGit(args, true)
+
+    if (output.exitCode !== 0) {
+      return []
+    }
+
+    return output.stdout
+      .trim()
+      .split('\n')
+      .filter(key => key.trim())
+  }
+
   async tryReset(): Promise<boolean> {
     const output = await this.execGit(['reset', '--hard', 'HEAD'], true)
     return output.exitCode === 0
@@ -623,7 +728,19 @@ class GitCommandManager {
       }
     }
     // Set the user agent
-    const gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
+    let gitHttpUserAgent = `git/${this.gitVersion} (github-actions-checkout)`
+
+    // Append orchestration ID if set
+    const orchId = process.env['ACTIONS_ORCHESTRATION_ID']
+    if (orchId) {
+      // Sanitize the orchestration ID to ensure it contains only valid characters
+      // Valid characters: 0-9, a-z, _, -, .
+      const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_')
+      if (sanitizedId) {
+        gitHttpUserAgent = `${gitHttpUserAgent} actions_orchestration_id/${sanitizedId}`
+      }
+    }
+
     core.debug(`Set git useragent to: ${gitHttpUserAgent}`)
     this.gitEnv['GIT_HTTP_USER_AGENT'] = gitHttpUserAgent
   }
diff --git src/git-source-provider.ts src/git-source-provider.ts
index 2d3513897..ec871784f 100644
--- src/git-source-provider.ts
+++ src/git-source-provider.ts
@@ -159,7 +159,6 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
     const fetchOptions: {
       filter?: string
       fetchDepth?: number
-      fetchTags?: boolean
       showProgress?: boolean
     } = {}
 
@@ -182,12 +181,35 @@ export async function getSource(settings: IGitSourceSettings): Promise<void> {
       if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
         refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
         await git.fetch(refSpec, fetchOptions)
+
+        // Verify the ref now matches. For branches, the targeted fetch above brings
+        // in the specific commit. For tags (fetched by ref), this will fail if
+        // the tag was moved after the workflow was triggered.
+        if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
+          throw new Error(
+            `The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
+              `The ref may have been updated after the workflow was triggered.`
+          )
+        }
       }
     } else {
       fetchOptions.fetchDepth = settings.fetchDepth
-      fetchOptions.fetchTags = settings.fetchTags
-      const refSpec = refHelper.getRefSpec(settings.ref, settings.commit)
+      const refSpec = refHelper.getRefSpec(
+        settings.ref,
+        settings.commit,
+        settings.fetchTags
+      )
       await git.fetch(refSpec, fetchOptions)
+
+      // For tags, verify the ref still points to the expected commit.
+      // Tags are fetched by ref (not commit), so if a tag was moved after the
+      // workflow was triggered, we would silently check out the wrong commit.
+      if (!(await refHelper.testRef(git, settings.ref, settings.commit))) {
+        throw new Error(
+          `The ref '${settings.ref}' does not point to the expected commit '${settings.commit}'. ` +
+            `The ref may have been updated after the workflow was triggered.`
+        )
+      }
     }
     core.endGroup()
 
diff --git src/misc/generate-docs.ts src/misc/generate-docs.ts
index 4b3c8ff52..b78f035c5 100644
--- src/misc/generate-docs.ts
+++ src/misc/generate-docs.ts
@@ -120,7 +120,7 @@ function updateUsage(
 }
 
 updateUsage(
-  'actions/checkout@v4',
+  'actions/checkout@v6',
   path.join(__dirname, '..', '..', 'action.yml'),
   path.join(__dirname, '..', '..', 'README.md')
 )
diff --git src/ref-helper.ts src/ref-helper.ts
index 58f929098..5130f53d7 100644
--- src/ref-helper.ts
+++ src/ref-helper.ts
@@ -76,55 +76,75 @@ export function getRefSpecForAllHistory(ref: string, commit: string): string[] {
   return result
 }
 
-export function getRefSpec(ref: string, commit: string): string[] {
+export function getRefSpec(
+  ref: string,
+  commit: string,
+  fetchTags?: boolean
+): string[] {
   if (!ref && !commit) {
     throw new Error('Args ref and commit cannot both be empty')
   }
 
   const upperRef = (ref || '').toUpperCase()
+  const result: string[] = []
+
+  // When fetchTags is true, always include the tags refspec
+  if (fetchTags) {
+    result.push(tagsRefSpec)
+  }
 
   // SHA
   if (commit) {
     // refs/heads
     if (upperRef.startsWith('REFS/HEADS/')) {
       const branch = ref.substring('refs/heads/'.length)
-      return [`+${commit}:refs/remotes/origin/${branch}`]
+      result.push(`+${commit}:refs/remotes/origin/${branch}`)
     }
     // refs/pull/
     else if (upperRef.startsWith('REFS/PULL/')) {
       const branch = ref.substring('refs/pull/'.length)
-      return [`+${commit}:refs/remotes/pull/${branch}`]
+      result.push(`+${commit}:refs/remotes/pull/${branch}`)
     }
     // refs/tags/
     else if (upperRef.startsWith('REFS/TAGS/')) {
-      return [`+${commit}:${ref}`]
+      if (!fetchTags) {
+        result.push(`+${ref}:${ref}`)
+      }
     }
     // Otherwise no destination ref
     else {
-      return [commit]
+      result.push(commit)
     }
   }
   // Unqualified ref, check for a matching branch or tag
   else if (!upperRef.startsWith('REFS/')) {
-    return [
-      `+refs/heads/${ref}*:refs/remotes/origin/${ref}*`,
-      `+refs/tags/${ref}*:refs/tags/${ref}*`
-    ]
+    result.push(`+refs/heads/${ref}*:refs/remotes/origin/${ref}*`)
+    if (!fetchTags) {
+      result.push(`+refs/tags/${ref}*:refs/tags/${ref}*`)
+    }
   }
   // refs/heads/
   else if (upperRef.startsWith('REFS/HEADS/')) {
     const branch = ref.substring('refs/heads/'.length)
-    return [`+${ref}:refs/remotes/origin/${branch}`]
+    result.push(`+${ref}:refs/remotes/origin/${branch}`)
   }
   // refs/pull/
   else if (upperRef.startsWith('REFS/PULL/')) {
     const branch = ref.substring('refs/pull/'.length)
-    return [`+${ref}:refs/remotes/pull/${branch}`]
+    result.push(`+${ref}:refs/remotes/pull/${branch}`)
   }
   // refs/tags/
+  else if (upperRef.startsWith('REFS/TAGS/')) {
+    if (!fetchTags) {
+      result.push(`+${ref}:${ref}`)
+    }
+  }
+  // Other refs
   else {
-    return [`+${ref}:${ref}`]
+    result.push(`+${ref}:${ref}`)
   }
+
+  return result
 }
 
 /**
@@ -170,8 +190,10 @@ export async function testRef(
   // refs/tags/
   else if (upperRef.startsWith('REFS/TAGS/')) {
     const tagName = ref.substring('refs/tags/'.length)
+    // Use ^{commit} to dereference annotated tags to their underlying commit
     return (
-      (await git.tagExists(tagName)) && commit === (await git.revParse(ref))
+      (await git.tagExists(tagName)) &&
+      commit === (await git.revParse(`${ref}^{commit}`))
     )
   }
   // Unexpected

Description

This PR introduces v5 and v6 of the actions/checkout GitHub Action with two major changes:

  1. Node.js 24 runtime (v5+): Updates the action to run on Node.js 24, requiring Actions Runner v2.327.1 or later.

  2. Improved credential security (v6): Instead of storing git credentials directly in .git/config, credentials are now persisted to a separate config file under $RUNNER_TEMP (git-credentials-<uuid>.config). The repository's .git/config uses includeIf.gitdir: directives to conditionally include these credentials. This approach:

    • Prevents credentials from being exposed in the repository's git config
    • Supports Docker container actions by configuring both host and container paths
    • Adds support for git worktrees

Additional changes include:

  • Tag verification now dereferences annotated tags using ^{commit}
  • Refs are now verified after fetch to detect if a tag/branch was moved after workflow trigger
  • User-agent now includes ACTIONS_ORCHESTRATION_ID for telemetry
  • fetchTags handling refactored to use explicit refspec instead of git's --no-tags/--tags flags

Possible Issues

  1. Breaking change for older runners: The Node.js 24 requirement means workflows running on self-hosted runners older than v2.327.1 will fail.

  2. Ref verification may break legitimate workflows: The new check that verifies refs point to expected commits after fetch could cause failures in edge cases where tags are legitimately moved (e.g., release tags that get updated).

  3. Container path assumptions: The hardcoded paths /github/workspace and /github/runner_temp assume the standard GitHub Actions container environment. Custom Docker setups may not work correctly.

  4. Test variable name mismatch: In git-auth-helper.test.ts, the test configureAuth_AcceptsGitHubServerUrlSetToGHEC passes a different test name string (configureAuth_AcceptsGitHubServerUrl) which appears to be a copy-paste error that was corrected in this PR.

Security Hotspots

  1. Credential file permissions (src/git-auth-helper.ts:340-345): The credentials config file is created via git config --file which may inherit default file permissions. The file contains the base64-encoded token and should have restrictive permissions (0600). The current implementation doesn't explicitly set file permissions.

  2. Credential file in RUNNER_TEMP (getCredentialsConfigPath): While RUNNER_TEMP is more secure than .git/config, the file persists until explicitly removed. If cleanup fails (caught exception in removeToken), credentials may remain on disk.

  3. Container path exposure: The credentials config path is written to .git/config twice - once for host path and once for container path (/github/runner_temp/...). This exposes the credential file location to anyone with read access to the repository's .git/config.

Changes

Changes

Workflow files (.github/workflows/*.yml):

  • Updated all actions/checkout references from v4/v4.1.6 to v6
  • Updated Node.js version from 20.x to 24.x
  • Added new tests for fetch-tags and worktree credentials
  • Fixed test checkout paths to avoid cleanup issues

action.yml:

  • Changed runtime from node20 to node24

src/git-auth-helper.ts:

  • Added credentialsConfigPath field to store path to external credentials file
  • configureToken() now creates credentials in $RUNNER_TEMP/git-credentials-<uuid>.config
  • Uses includeIf.gitdir: in .git/config to conditionally include credentials
  • Configures both host and container paths for Docker action support
  • Added worktree support with includeIf.gitdir:*/worktrees/*.path
  • configureSubmoduleAuth() now adds includeIf entries to submodule configs
  • New getCredentialsConfigPath() generates unique filename with UUID
  • removeToken() now cleans up includeIf entries and deletes credentials file
  • Added testCredentialsConfigPath() to validate credentials file pattern
  • Improved cleanup logging

src/git-command-manager.ts:

  • Added configFile parameter to config() method
  • fetch() now always uses --no-tags (tags fetched via explicit refspec)
  • Added getSubmoduleConfigPaths() to retrieve submodule config file paths
  • Added tryConfigUnsetValue() to remove specific config values
  • Added tryGetConfigValues() and tryGetConfigKeys() for config queries
  • User-agent now includes ACTIONS_ORCHESTRATION_ID (sanitized)

src/git-source-provider.ts:

  • Removed fetchTags from fetch options (now handled via refspec)
  • Added post-fetch verification that ref points to expected commit

src/ref-helper.ts:

  • getRefSpec() now accepts fetchTags parameter
  • When fetchTags=true, adds +refs/tags/*:refs/tags/* to refspec
  • Avoids redundant tag refspecs when fetchTags covers them
  • testRef() now uses ^{commit} to dereference annotated tags

Test files:

  • Updated tests to verify credentials are in separate config file
  • Added tests for includeIf entries in git config
  • Added tests for submodule credential removal
  • Added tests for orchestration ID in user-agent
  • Added verify-fetch-tags.sh and verify-worktree.sh test scripts
  • Updated submodule verification scripts to use --includes flag
sequenceDiagram
    participant Workflow
    participant Checkout as actions/checkout
    participant AuthHelper as GitAuthHelper
    participant Git as GitCommandManager
    participant FS as File System

    Workflow->>Checkout: Run checkout action
    Checkout->>AuthHelper: configureAuth()
    AuthHelper->>AuthHelper: getCredentialsConfigPath()
    Note over AuthHelper: Generate UUID filename<br/>git-credentials-{uuid}.config
    AuthHelper->>Git: config(tokenKey, placeholder, configFile)
    Git->>FS: Write placeholder to RUNNER_TEMP/git-credentials-*.config
    AuthHelper->>FS: Replace placeholder with actual token
    AuthHelper->>Git: config(includeIf.gitdir:hostPath.path, credPath)
    Git->>FS: Add includeIf to .git/config (host path)
    AuthHelper->>Git: config(includeIf.gitdir:containerPath.path, credPath)
    Git->>FS: Add includeIf to .git/config (container path)
    
    Note over Checkout: Git operations now use<br/>credentials via includeIf
    
    Checkout->>Git: fetch(refSpec, options)
    Git-->>Checkout: Fetch complete
    Checkout->>AuthHelper: testRef(git, ref, commit)
    AuthHelper->>Git: revParse(ref^{commit})
    Git-->>AuthHelper: Actual commit SHA
    AuthHelper-->>Checkout: Verify ref matches expected commit
    
    Note over Checkout: Post action cleanup
    
    Checkout->>AuthHelper: removeAuth()
    AuthHelper->>Git: tryGetConfigKeys(includeIf.gitdir:)
    Git-->>AuthHelper: List of includeIf keys
    AuthHelper->>Git: tryConfigUnsetValue(key, credPath)
    Git->>FS: Remove includeIf entries from .git/config
    AuthHelper->>FS: Delete RUNNER_TEMP/git-credentials-*.config
Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants