Documentation

Git HTTPS Access Token from an Environment Variable (Without Storing It)

Use a token from an environment variable for Git HTTPS remotes via GIT_CONFIG_COUNT and a credential helper, so it never lands in .git/config.

A
Adam Birds
Published 24 October 20262 min read0 views

To let a container, CI job or service pull and push over HTTPS with an access token, without the token ending up in the remote URL, .git/config or a credentials file, supply it through a credential helper defined entirely in environment variables.

The usual shortcuts leak the token:

  • https://<api-token>@github.com/<owner>/<repo>.git is saved in .git/config and appears in git remote -v output and some error messages.
  • git config credential.helper store writes it in plain text to ~/.git-credentials.

How it works

Since Git 2.31, Git reads extra configuration from GIT_CONFIG_COUNT, GIT_CONFIG_KEY_<n> and GIT_CONFIG_VALUE_<n>. These settings apply only to processes that inherit the environment, and nothing is written to disk. A credential.helper value that starts with ! is run as a shell snippet: Git calls it with get and reads username= and password= lines from its output.

Fix

Set these variables in the environment of whatever runs Git:

export GIT_TOKEN="<api-token>"
export GIT_TERMINAL_PROMPT=0
export GIT_CONFIG_COUNT=2
export GIT_CONFIG_KEY_0=credential.helper
export GIT_CONFIG_VALUE_0=
export GIT_CONFIG_KEY_1=credential.helper
export GIT_CONFIG_VALUE_1='!f() { test "$1" = get || exit 0; echo username=x-access-token; echo "password=$GIT_TOKEN"; }; f'

What each part does:

  • The empty first value clears any helpers inherited from the system or global config (such as a desktop keychain helper), so only this helper is asked.
  • The helper refers to $GIT_TOKEN instead of containing the token, so git config --list never shows the secret.
  • test "$1" = get || exit 0 ignores the store and erase calls Git makes after a request succeeds or fails.
  • GIT_TERMINAL_PROMPT=0 makes Git fail instead of waiting for a password prompt that no one can answer.

The remote stays a plain URL:

git remote add origin https://github.com/<owner>/<repo>.git
git push origin main

From a program

When a service runs Git as a subprocess, pass the variables in that subprocess’s environment only:

import os
import subprocess

HELPER = (
    '!f() { test "$1" = get || exit 0; '
    'echo "username=$GIT_USERNAME"; echo "password=$GIT_TOKEN"; }; f'
)

env = os.environ | {
    "GIT_TERMINAL_PROMPT": "0",
    "GIT_USERNAME": "x-access-token",
    "GIT_TOKEN": token,
    "GIT_CONFIG_COUNT": "2",
    "GIT_CONFIG_KEY_0": "credential.helper",
    "GIT_CONFIG_VALUE_0": "",
    "GIT_CONFIG_KEY_1": "credential.helper",
    "GIT_CONFIG_VALUE_1": HELPER,
}
result = subprocess.run(
    ["git", "push", "origin", "main"],
    cwd=repo_path, env=env, capture_output=True, text=True,
)
if result.returncode != 0:
    # Redact before logging, in case the token appears in an error.
    raise RuntimeError(result.stderr.replace(token, "***"))

Verify

Ask Git which credentials it would use:

printf 'protocol=https\nhost=github.com\n\n' | git credential fill

The output should include username=x-access-token and the token as password=. Then confirm where the helper comes from and that nothing was stored:

git config --show-origin --get-all credential.helper   # helper listed with origin "command line:"
grep -c "<part-of-token>" .git/config                  # 0

Notes

  • Requires Git 2.31 or later. Tested with Git 2.43 and 2.54.
  • For GitHub, a fine-grained personal access token limited to one repository, with Contents read and write, is enough to pull and push. GitHub should accept any non-empty username with a token; x-access-token is the conventional value.
  • Every child process inherits these variables. If other code runs in the same environment, set them only on the Git subprocess, as in the Python example.
  • For SSH remotes, the equivalent is a deploy key: write the private key to a file readable only by its owner and set GIT_SSH_COMMAND="ssh -i <key-file> -o IdentitiesOnly=yes".
Last updated: 24 October 2026v1