Security Implications of Using Tokens with actions/checkout: Risks and Mitigations

Using tokens with actions/checkout writes credentials to the runner's Git configuration, creating exposure risks through log leaks, residual files, and over-scoped permissions that require specific mitigations to secure.

The actions/checkout GitHub Action is the standard mechanism for retrieving repository code in CI/CD workflows. When accessing private repositories or authenticated submodules, it accepts a personal access token (PAT) via the token input defined in action.yml. Understanding how this credential is processed in src/input-helper.ts and src/git-auth-helper.ts is essential for preventing supply chain attacks and credential leakage.

How actions/checkout Handles Authentication Tokens

In src/input-helper.ts (lines 56-58), the action reads the token input and stores it in the settings object as authToken defined in src/git-source-settings.ts. The actual authentication implementation resides in src/git-auth-helper.ts (lines 55-65), where the token is encoded as a base64 basic-auth header and written to Git's configuration as an HTTP extra-header.

The action immediately masks the token using core.setSecret (line 62) to prevent accidental exposure in workflow logs. However, because the credential is physically written to disk in Git's config files, several attack surfaces emerge if proper precautions are not taken.

Critical Security Risks and Code-Level Mitigations

Token Exposure Through Log Injection

Although core.setSecret masks the raw token value, any subsequent console.log of the Git configuration file or environment variables could reveal the base64-encoded credential. The token is formatted as x-access-token:${token} before encoding.

Mitigation: Avoid custom logging of Git config files or environment inspection scripts after the checkout step completes.

Credentials Persisting on the Runner

The token is written to a temporary credentials configuration path (credentialsConfigPath) as implemented in src/git-auth-helper.ts (lines 322-470). The removeAuth function attempts to clean up these files, but if a workflow terminates abnormally or the cleanup step fails, the token may remain on disk—particularly problematic for self-hosted runners.

Mitigation: Set persist-credentials: false (the default) and ensure workflows terminate cleanly without forced cancellations during the checkout phase.

Submodule Configuration Leakage

When persist-credentials is explicitly set to true, the token propagates to submodule Git configurations (lines 61-66 in git-auth-helper.ts). This exposes the credential to other workflows, containers, or processes that might access the workspace directory.

Mitigation: Only enable persist-credentials when private submodules require authentication. For public repositories or internal submodules, explicitly disable this setting.

Global Git Config Contamination

The configureGlobalAuth function (lines 28-46) can write the token to the global ~/.gitconfig if global authentication mode is activated. On shared runners or systems hosting multiple jobs, this leaks credentials across workflow boundaries.

Mitigation: Use the default per-run Git configuration. Avoid global authentication unless explicitly required by your infrastructure.

Scope Over-Granting and Token Reuse

The action does not enforce minimal token scope. Users frequently provide PATs with full repo access when only contents:read is required. If compromised, an attacker could push, delete, or alter any repository accessible to that token.

Mitigation: Generate short-lived, read-only PATs scoped specifically to repo:public_repo or contents:read. Prefer the built-in ${{ secrets.GITHUB_TOKEN }} when possible, as it provides minimal, workflow-scoped permissions.

Secure Configuration Examples

Use the built-in GITHUB_TOKEN with credential persistence disabled for standard operations:

- uses: actions/checkout@v4
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    persist-credentials: false   # Prevents token storage in submodule configs

When private submodules require authentication, use a minimally scoped PAT:

- name: Checkout with submodule auth
  uses: actions/checkout@v4
  with:
    token: ${{ secrets.SUBMODULE_PAT }}   # PAT limited to repo:read

    persist-credentials: true            # Only enable when required

The following TypeScript demonstrates how the action encodes the token internally, illustrating why raw config output should be avoided:

// Implementation detail from git-auth-helper.ts
import * as core from '@actions/core';
import { Buffer } from 'buffer';

const token = core.getInput('token', { required: true });
const encoded = Buffer.from(`x-access-token:${token}`, 'utf8').toString('base64');
core.setSecret(encoded); // Masks in logs, but value exists in Git config
// Git uses header: AUTHORIZATION: basic <encoded>

Summary

  • Token storage: Credentials are written to Git config via git-auth-helper.ts and require proper cleanup via removeAuth (lines 322-470).
  • Persistence: Keep persist-credentials: false unless submodules explicitly require authentication, preventing credential leakage to subsequent steps.
  • Scope minimization: Use read-only tokens scoped to contents:read rather than full repository access.
  • Global config: Avoid configureGlobalAuth (lines 28-46) to prevent cross-job contamination on shared runners.
  • Cleanup assurance: Rely on the built-in cleanup logic rather than custom scripts that might leave credentials in credentialsConfigPath.

Frequently Asked Questions

Does actions/checkout encrypt the token at rest?

No. The token is base64-encoded as a basic auth header but remains plaintext in the Git configuration file during job execution. While core.setSecret masks the value in workflow logs, the credential exists unencrypted on the runner's filesystem until the removeAuth cleanup function executes.

What happens if persist-credentials is not specified?

By default, persist-credentials is false, meaning the action removes the token from Git config immediately after checkout completes. This is the safest configuration for standard repository access and prevents the credential from being available to subsequent workflow steps or submodules.

Can I use the default GITHUB_TOKEN for private submodules in other repositories?

No. The automatic ${{ secrets.GITHUB_TOKEN }} only has access to the current repository. To checkout private submodules hosted in different repositories, you must use a Personal Access Token with repo or contents:read scope for those specific repositories, configured with minimal permissions and short expiration.

How can I verify that credentials were properly removed?

The removeAuth function in src/git-auth-helper.ts handles cleanup by deleting the temporary credential files. You can verify successful removal by adding a post-checkout step that runs git config --global --get-regexp 'http.*extraheader'—it should return no output if persist-credentials is false or if cleanup succeeded.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →