# How actions/checkout Handles SSH Authentication for Submodules in GitHub Actions

> actions/checkout securely handles SSH authentication for submodules. Learn how it uses core.sshCommand and includeIf for seamless Git submodule access.

- Repository: [GitHub Actions/checkout](https://github.com/actions/checkout)
- Tags: how-to-guide
- Published: 2026-08-29

---

**actions/checkout** writes the SSH private key to a temporary file, constructs a custom SSH command, and injects this configuration into both the parent repository and all submodules using Git's `core.sshCommand` and `includeIf` directives, ensuring seamless SSH authentication without manual Git configuration.

Fetching Git submodules over SSH in GitHub Actions requires secure credential handling that persists across nested repositories. The **actions/checkout** action automates this process by dynamically configuring Git authentication at runtime according to the source code in `actions/checkout`. When you provide an SSH key, the action intercepts Git commands to use custom SSH configuration, propagating these settings to every submodule automatically.

## Parsing SSH Inputs and Initial Setup

The authentication process begins by reading SSH-specific inputs from your workflow configuration.

### Reading SSH-Related Inputs

In [`src/input-helper.ts`](https://github.com/actions/checkout/blob/main/src/input-helper.ts) (lines 160-164), the action parses the optional SSH parameters you provide:

```typescript
result.sshKey         = core.getInput('ssh-key')
result.sshKnownHosts  = core.getInput('ssh-known-hosts')
result.sshStrict      = (core.getInput('ssh-strict') || 'true').toUpperCase() === 'TRUE'
result.sshUser        = core.getInput('ssh-user')

```

If `ssh-key` is present, the action switches to **SSH-based authentication** for both the main repository and all submodules.

### Temporary Key Storage

The action immediately writes the private key to a temporary file on the runner. In [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts) (lines 251-259), the code generates a unique path under `$RUNNER_TEMP` and sets strict file permissions (typically 0o600) to prevent unauthorized access:

```typescript
const sshKeyPath = path.join(runnerTemp, uniqueId)
await fs.promises.writeFile(sshKeyPath, sshKey, { mode: 0o600 })

```

Simultaneously, the action constructs a `known_hosts` file (lines 298-304) combining your provided `ssh-known-hosts` input with GitHub's default host keys to prevent man-in-the-middle attacks.

## Constructing the SSH Command

Once the key material is secured, **actions/checkout** builds a custom SSH command that overrides Git's default SSH behavior.

### Building GIT_SSH_COMMAND

In [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts) (lines 215-224), the action assembles the command string:

```typescript
this.sshCommand = `"${sshPath}" -i "${this.sshKeyPath}"`
if (this.settings.sshStrict) {
    this.sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'
}
if (this.settings.sshUser) {
    this.sshCommand += ` -l "${this.sshUser}"`
}

```

### Persisting Configuration to Git Config

The action stores this command in two locations to ensure all Git subprocesses use it. First, it sets the `GIT_SSH_COMMAND` environment variable (lines 312-313). Second, it writes to the repository's Git config as `core.sshCommand` (lines 317-322):

```typescript
core.info(`Temporarily overriding GIT_SSH_COMMAND=${this.sshCommand}`)
this.git.setEnvironmentVariable('GIT_SSH_COMMAND', this.sshCommand)
await this.git.config('core.sshCommand', this.sshCommand)

```

This dual configuration ensures that both the initial clone and any subsequent Git operations use the specified private key.

## Propagating Authentication to Submodules

Submodules present a unique challenge because they maintain separate Git configurations. **actions/checkout** solves this using Git's `includeIf` directive.

### The includeIf Configuration Pattern

In [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts) (lines 171-197), the action prepares a shared credentials configuration. When using SSH, this file contains the `core.sshCommand` setting. For each submodule, the action adds an entry to `.git/modules/<name>/config` that conditionally includes the shared file:

```ini
[includeIf "gitdir:/path/to/submodule/.git"]
    path = /path/to/git-credentials-*.config

```

This pattern ensures that when Git operates inside a submodule directory, it automatically inherits the parent's SSH configuration without duplicating sensitive data.

### Submodule Configuration Injection

During the checkout process in [`src/git-source-provider.ts`](https://github.com/actions/checkout/blob/main/src/git-source-provider.ts) (lines 275-285), the action iterates through all submodules using `git submodule foreach` to inject the SSH command directly:

```typescript
await git.submoduleForeach(
    `config --local core.sshCommand "${this.sshCommand}"`,
    true
)

```

This guarantees that `git submodule update` commands authenticate using the provided SSH key, regardless of whether the submodule URL uses `git@github.com:` or `ssh://` formats.

## Complete Workflow Examples

### Basic SSH Authentication for Submodules

The following configuration recursively fetches submodules using an SSH key stored as a repository secret:

```yaml
steps:
  - uses: actions/checkout@v4
    with:
      submodules: recursive
      ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
      ssh-known-hosts: |
        github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQ...
      ssh-strict: true

```

Under the hood, this creates:
- `$RUNNER_TEMP/<unique>.pem` containing your private key
- `$RUNNER_TEMP/<unique>_known_hosts` with verified host keys
- `GIT_SSH_COMMAND="ssh -i $RUNNER_TEMP/<unique>.pem -o StrictHostKeyChecking=yes -o CheckHostIP=no"`

### Mixed HTTPS and SSH Authentication

You can combine token-based HTTPS authentication with SSH for specific submodules:

```yaml
steps:
  - uses: actions/checkout@v4
    with:
      submodules: true
      token: ${{ secrets.GITHUB_TOKEN }}
      ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}
      persist-credentials: true

```

According to the source code in [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts) (lines 185-197), this creates two credential mechanisms:
1. A `git-credentials-*.config` file for HTTPS URLs (lines 185-197)
2. The `core.sshCommand` configuration for SSH URLs (lines 215-224)

The `includeIf` entries route each submodule to the appropriate authentication method based on its remote URL.

### Disabling Credential Persistence

For security-hardened environments, disable token persistence when using SSH-only authentication:

```yaml
steps:
  - uses: actions/checkout@v4
    with:
      submodules: true
      persist-credentials: false
      ssh-key: ${{ secrets.SSH_PRIVATE_KEY }}

```

This prevents the action from writing the `git-credentials-*.config` file, leaving only the SSH command in the Git configuration.

## Security and Cleanup Procedures

After the job completes, **actions/checkout** removes all authentication artifacts to prevent credential leakage between workflows. In [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts), the `removeAuth()` method performs two critical operations:

**Removing Submodule Configurations** (lines 486-495):
The action iterates through all submodules and strips the `includeIf` entries added during setup, ensuring subsequent jobs cannot access the previous SSH key.

**Clearing Core SSH Command** (lines 532-538):
The action unsets `core.sshCommand` from the main repository config and deletes the temporary key files from `$RUNNER_TEMP`.

This cleanup executes automatically as a post-job step, even if your workflow fails or is cancelled.

## Summary

- **Input Processing**: [`src/input-helper.ts`](https://github.com/actions/checkout/blob/main/src/input-helper.ts) parses `ssh-key`, `ssh-known-hosts`, `ssh-strict`, and `ssh-user` to determine authentication strategy.
- **Command Construction**: [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts) builds a temporary SSH command with strict host checking and writes it to both environment variables and Git config.
- **Submodule Propagation**: The action uses Git's `includeIf` directive and `git submodule foreach` to inject `core.sshCommand` into every submodule's local configuration.
- **Dual Authentication**: When both `token` and `ssh-key` are present, the action maintains separate credential files and routes each submodule to the appropriate mechanism.
- **Automatic Cleanup**: Post-job execution removes all temporary keys, known-hosts files, and configuration entries from [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts) lines 486-538.

## Frequently Asked Questions

### How does actions/checkout pass SSH keys to nested submodules?

The action writes the SSH private key to a temporary file and constructs a custom SSH command. It then uses `git submodule foreach` to execute `git config --local core.sshCommand` in each submodule directory, as implemented in [`src/git-source-provider.ts`](https://github.com/actions/checkout/blob/main/src/git-source-provider.ts) (lines 275-285). This ensures all nested repositories use the same SSH key without requiring separate secrets for each submodule.

### Can I use both HTTPS tokens and SSH keys simultaneously?

Yes. When you provide both `token` and `ssh-key` inputs, **actions/checkout** creates a credential helper file for HTTPS URLs and an SSH command configuration for SSH URLs. The `includeIf` entries in each submodule's Git config route authentication requests to the appropriate mechanism based on the remote URL's protocol, as handled in [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts) (lines 171-197).

### Where are the temporary SSH keys stored during the workflow?

The action stores temporary files in the `$RUNNER_TEMP` directory with unique identifiers. Specifically, [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts) (lines 251-259) writes the key to `path.join(runnerTemp, uniqueId)` with 0o600 permissions, and [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts) (lines 298-304) creates a companion known-hosts file. These paths are injected into the SSH command but never logged or exposed in workflow outputs.

### How do I disable strict host key checking for submodules?

Set `ssh-strict: false` in your workflow inputs. In [`src/git-auth-helper.ts`](https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts) (lines 215-224), the code checks this setting and omits the `-o StrictHostKeyChecking=yes` flag from the SSH command when disabled. This allows connections to hosts not present in your `ssh-known-hosts` input or the default GitHub host keys, though this reduces security against man-in-the-middle attacks.