Skip to content

Git credential helpers and CI/CD token management

Automated continuous integration runners, Docker build containers, and developer workstations require secure access to remote Git repositories. Embedding static personal access tokens into repository clone URLs causes tokens to leak into build logs, shell history, .git/config files, and shared process tables.

Git credential helpers solve this problem by decoupling repository URLs from authentication secrets. Git queries configured helpers on demand during network operations such as git fetch, git pull, and git push.


The Git credential helper protocol

Git communicates with credential helpers over standard input and standard output using a line-oriented key-value protocol. Each line contains a key, an equals sign, and a value. An empty line signals the end of the payload.

key=value\n
key=value\n
\n

Supported protocol keys

Key Description
protocol The network scheme (usually https or http).
host Hostname and optional port (for example git.local.sneakysquid.xyz or github.com).
path Repository path (for example hermes/git-master.git). Git sends this when credential.useHttpPath is true.
username The authentication user account or token identifier.
password The plaintext password, personal access token, or OAuth bearer token.
password_expiry_utc Unix epoch timestamp indicating when the token expires (supported in Git 2.40+).
quit Tells Git to stop querying downstream helpers in the chain if set to 1 or true.

Helper actions

Git executes a helper executable with one of three positional arguments:

  1. get: Git prompts the helper to provide credentials. The helper reads repository details from standard input and writes username=... and password=... to standard output.
  2. store: Git notifies the helper that authentication succeeded. The helper records the credentials in persistent storage if supported.
  3. erase: Git notifies the helper that authentication failed. The helper purges the invalid credentials from its storage.

Built-in credential helpers

Git provides several built-in helper backends.

1. In-memory cache helper (git-credential-cache)

The cache helper stores credentials in a background daemon memory cache using Unix domain sockets. It never writes credentials to disk.

# Cache credentials in memory for 1 hour (3600 seconds)
git config --global credential.helper "cache --timeout=3600"

# Specify a custom socket path for isolated CI containers
git config --global credential.helper "cache --timeout=1800 --socket=/tmp/git-cache.sock"

To clear cached credentials immediately, run:

git credential-cache exit

2. Disk store helper (git-credential-store)

The store helper saves credentials in an unencrypted flat file (default ~/.git-credentials).

# Configure standard store helper
git config --global credential.helper "store --file ~/.git-custom-credentials"

The file stores entries in standard URL format:

https://oauth2:glpat-abcdef123456@gitlab.com
https://devops-bot:ghp_0123456789abcdef@github.com

Lock file permissions to owner read/write only:

chmod 600 ~/.git-credentials

3. Operating system secret services

Production workstations should use native system keychains rather than unencrypted flat files:

# Linux (libsecret / GNOME Keyring / KWallet)
git config --global credential.helper libsecret

# macOS Keychain
git config --global credential.helper osxkeychain

# Windows Credential Manager
git config --global credential.helper wincred

Scoped credential configuration

You can scope credential helpers to specific domains, subdomains, or individual repository paths instead of applying a single helper globally.

# Global fallback
[credential]
    helper = cache --timeout=300

# Scoped to internal Forgejo instance
[credential "https://git.local.sneakysquid.xyz"]
    helper = store --file=/secrets/forgejo.creds
    useHttpPath = false

# Scoped to specific GitHub organization
[credential "https://github.com/my-enterprise-org"]
    helper = "!/usr/local/bin/vault-token-helper"
    useHttpPath = true

When multiple helpers exist for a scope, Git executes them in order until one returns valid credentials.


Custom credential helpers for enterprise CI/CD

In automated pipelines, build agents can fetch dynamic short-lived tokens from secret managers (such as HashiCorp Vault, AWS Secrets Manager, or Kubernetes secrets) just in time.

Custom helper implementation in Bash

Create a standalone executable /usr/local/bin/ci-vault-git-helper:

#!/usr/bin/env bash
set -euo pipefail

# Read key-value pairs from standard input
declare -A INPUT_VARS
while IFS='=' read -r key value; do
    [[ -z "$key" ]] && break
    INPUT_VARS["$key"]="$value"
done

ACTION="${1:-}"

case "$ACTION" in
    get)
        TARGET_HOST="${INPUT_VARS[host]:-}"
        TARGET_PATH="${INPUT_VARS[path]:-}"

        # Fetch dynamic token from Vault or environment
        if [[ "$TARGET_HOST" == "git.local.sneakysquid.xyz" ]]; then
            TOKEN="${FORGEJO_TOKEN:-}"
            if [[ -z "$TOKEN" && -f "/var/run/secrets/tokens/forgejo" ]]; then
                TOKEN="$(cat /var/run/secrets/tokens/forgejo)"
            fi

            if [[ -n "$TOKEN" ]]; then
                echo "username=oauth2"
                echo "password=${TOKEN}"
                echo "quit=true"
            fi
        fi
        ;;
    store)
        # Dynamic CI tokens are read-only; no-op on store
        ;;
    erase)
        # Optionally log or alert on failed authentication
        ;;
    *)
        exit 0
        ;;
esac

Make the script executable:

chmod +x /usr/local/bin/ci-vault-git-helper

Register the script with Git using the exclamation mark syntax:

git config --global credential.helper "!/usr/local/bin/ci-vault-git-helper"

The exclamation mark ! tells Git to run the command through the system shell rather than searching Git's built-in exec-path.


CI/CD runner authentication patterns

DevOps engineers use three distinct patterns to authenticate Git on ephemeral build agents.

Pattern 1: Dynamic HTTP extra header (zero storage)

The cleanest pattern injects an HTTP Authorization header directly into git commands using -c. This bypasses the credential helper system entirely and leaves zero traces on disk.

# Forgejo / Gitea token injection
git -c http.extraHeader="Authorization: token ${FORGEJO_TOKEN}" clone https://git.local.sneakysquid.xyz/hermes/git-master.git

# GitHub / GitLab Bearer token injection
git -c http.extraHeader="Authorization: Bearer ${CI_JOB_TOKEN}" fetch origin

To configure this for an entire pipeline step in GitHub Actions or Forgejo Actions:

git config --global http."https://git.local.sneakysquid.xyz/".extraHeader "Authorization: token ${FORGEJO_TOKEN}"

Pattern 2: Global URL rewriting (insteadOf)

You can rewrite repository URLs transparently to include basic authentication tokens.

git config --global url."https://oauth2:${FORGEJO_TOKEN}@git.local.sneakysquid.xyz/".insteadOf "https://git.local.sneakysquid.xyz/"

Any subsequent call to git clone https://git.local.sneakysquid.xyz/repo.git automatically uses the rewritten authenticated URL.

Note: If git commands log their invoked URLs, the token will appear in logs. Use Pattern 1 when log masking is unavailable.

Pattern 3: Ephemeral in-memory helper injection

For multi-step pipelines requiring submodules and multiple remote fetches, initialize a memory cache before steps run:

# Start isolated daemon socket
SOCKET_PATH="/tmp/ci-git-cache-$$.sock"
git config credential.helper "cache --timeout=900 --socket=${SOCKET_PATH}"

# Pre-populate credentials
printf "protocol=https\nhost=git.local.sneakysquid.xyz\nusername=oauth2\npassword=%s\n\n" "$FORGEJO_TOKEN" | git credential approve

Clean up at job completion:

git credential-cache --socket="${SOCKET_PATH}" exit

Debugging and testing credential helpers

You can test credential helpers directly on the terminal without triggering network operations.

Testing credential resolution (get / fill)

printf "protocol=https\nhost=git.local.sneakysquid.xyz\n\n" | git credential fill

Expected output:

protocol=https
host=git.local.sneakysquid.xyz
username=oauth2
password=secret_token_value

Manually storing credentials (store / approve)

printf "protocol=https\nhost=git.local.sneakysquid.xyz\nusername=devops\npassword=mysecret\n\n" | git credential approve

Manually clearing credentials (erase / reject)

printf "protocol=https\nhost=git.local.sneakysquid.xyz\n\n" | git credential reject

Tracing network authentication

To inspect the raw HTTP headers and TLS handshake without printing tokens:

GIT_CURL_VERBOSE=1 GIT_TRACE=1 git ls-remote https://git.local.sneakysquid.xyz/hermes/git-master.git

Git masks Authorization: Basic ... and Authorization: Bearer ... headers in trace outputs by default.


Security auditing and leak remediation

Embedding passwords or tokens inside .git/config is a critical vulnerability. If a repository directory is shared, archived, or exposed via a web server misconfiguration, those credentials leak immediately.

1. Audit repository for embedded credentials

Check local and global configuration for plaintext tokens:

# Check remote URLs in local config
git config --local --get-regexp '^remote\..*\.url$' | grep -E 'https?://[^:]+:[^@]+@'

# Check for hardcoded extraHeader credentials
git config --list --show-origin | grep -i 'extraHeader'

2. Sanitize repository remote URLs

Strip embedded credentials and replace them with standard HTTPS URLs:

# Vulnerable remote URL
git remote set-url origin https://bot:glpat-123456789@git.local.sneakysquid.xyz/org/repo.git

# Sanitized remote URL
git remote set-url origin https://git.local.sneakysquid.xyz/org/repo.git