When a long-running cloud Mac builds multiple repositories in succession, the greatest risk is not always a token appearing directly in a script. Often, the real danger is that one job silently leaves credentials behind for the next. Common residue includes global Git configuration, remote URLs containing authentication data, the login keychain, and temporary scripts that were never deleted. The goal is not merely to “remove a variable after the build,” but to ensure that credentials can only ever exist within the boundary of a single job.
First, determine where credentials are stored
Do not rush to modify the pipeline. First, identify which configuration Git is actually reading without exposing any secret values. --show-origin reports the source of each setting, making it possible to distinguish system-level, user-level, and repository-level configuration.
set +x
git config --show-origin --get-all credential.helper || true
git config --show-origin --get-regexp '^(credential|http)\.' || true
git config --show-origin --get-regexp '^url\..*\.insteadof$' || true
Focus on three types of signals:
- whether
credential.helperpoints to a persistent keychain; - whether
http.extraHeaderhas been written to global or repository configuration; - whether
url.*.insteadOfrewrites a normal URL into one containing authentication data.
Remote URLs must also be checked, but they should never be written to logs verbatim. Instead, detect whether the URL matches the scheme://userinfo@host pattern and fail immediately if it does, without printing the full address.
remote_url="$(git remote get-url origin)"
case "$remote_url" in
*://*@*)
printf '%s
' "Remote URL contains embedded credentials" >&2
exit 1
;;
esac
The first rule of an audit script is to report only that credentials may be present. Never expose the credentials themselves in build logs just to prove that a problem exists.
Create an isolated HOME for every job
Git derives the path to user-level configuration from HOME. Allowing every job to share the runner account’s HOME also makes them share .gitconfig, credential helper settings, and a large amount of tool state. A safer approach is to create a temporary HOME with 700 permissions for each job and explicitly specify the global Git configuration file.
set -eu
ORIGINAL_HOME="$HOME"
JOB_HOME="$(mktemp -d "${TMPDIR%/}/git-job.XXXXXX")"
chmod 700 "$JOB_HOME"
export HOME="$JOB_HOME"
export XDG_CONFIG_HOME="$JOB_HOME/.config"
export GIT_CONFIG_GLOBAL="$JOB_HOME/.gitconfig"
export GIT_TERMINAL_PROMPT=0
mkdir -p "$XDG_CONFIG_HOME"
This does not isolate system-level Git configuration, so the helper chain must still be reset explicitly. Git supports multiple credential.helper values. Setting an empty value first clears helpers inherited from lower-priority configuration before the job-specific implementation is added.
Keep the repository outside the temporary HOME
HOME defines the boundary for credentials and tool state; it does not also need to serve as the workspace. Source directories should remain under the runner’s workspace management so that storage usage can be controlled and artifacts can be collected. Keeping the two separate ensures that deleting HOME does not remove build results, while cleaning the workspace does not leave user-level authentication settings behind.
If the pipeline runs jobs concurrently, every concurrent slot must call mktemp independently. Do not reuse a fixed directory based on the repository name. A fixed directory can be inherited by the next job after an abnormal exit and may also allow two jobs to modify the same .gitconfig simultaneously.
Provide the token only when Git requests it
The token should be injected into the environment through the CI system’s secret-variable mechanism, and scripts should reference only the variable name. Do not embed the token in the clone URL or persist an authentication header with git config --global http.extraHeader.
The helper below responds only to Git get requests. The configuration file stores the variable-reference logic, not the token value:
: "${CI_GIT_USER:?CI_GIT_USER is required}"
: "${CI_GIT_TOKEN:?CI_GIT_TOKEN is required}"
git config --global credential.helper ""
git config --global --add credential.helper \
'!f() {
if [ "$1" = get ]; then
printf "username=%s
password=%s
" \
"$CI_GIT_USER" "$CI_GIT_TOKEN"
fi
}; f'
Disable command tracing and interactive fallback
Run set +x before accessing secret variables. Otherwise, the shell may write commands containing expanded variable values to the log. GIT_TERMINAL_PROMPT=0 is equally important: if the token is missing or invalid, the job must fail clearly instead of hanging on an invisible interactive prompt.
Also check whether build wrappers automatically dump environment variables. Diagnostics should report only whether a variable is set—for example, by checking whether its length is greater than zero. They must not print the variable’s contents, authentication headers, or complete remote URLs.
For automated tasks that require write access, use separate credentials for reading source code and pushing artifacts. A job that only performs a checkout does not need write access, and a publishing job should not receive permissions beyond the target repository and the operations it must perform.
Use trap to cover every exit path
Placing rm -rf only at the end of a script does not cover early failures, timeout termination, or manual cancellation. Register a trap immediately after creating the temporary directory. In the cleanup function, unset the variables first and then delete the job HOME.
cleanup_git_credentials() {
set +e
unset CI_GIT_TOKEN CI_GIT_USER
if [ -n "${JOB_HOME:-}" ] && [ -d "$JOB_HOME" ]; then
rm -rf "$JOB_HOME"
fi
}
trap cleanup_git_credentials EXIT HUP INT TERM
The cleanup function must be safe to run repeatedly and must not fail if the directory no longer exists. The deletion target must also meet two conditions: the variable is nonempty, and it actually points to a directory. Do not use broad keychain deletion commands or clear the entire login keychain, because it may contain other items required by the same runner account.
If an older pipeline used a persistent helper, first inventory it precisely by host and account, then plan a one-time migration. While the old and new approaches coexist, every job must verify the source of its active helpers. This prevents a system wrapper from copying the old configuration back after the temporary HOME has already been enabled.
Turn residue checks into a build gate
Successful cleanup cannot be inferred merely from the absence of script errors. Add a post-exit check around the runner, or have the executor verify the following when reclaiming the job:
- the temporary HOME has been deleted;
- the workspace
.git/configcontains no authentication headers or remote URLs with user information; - the job log contains no known fingerprints of secret variables;
- Git configuration sources include only the expected system configuration and the current temporary configuration;
- a subsequent blank job cannot read repository credentials left by the previous job.
For testing, assign a fixed marker value with no permissions to the test token, run failure scenarios in an isolated environment, and then scan the logs and file system. The purpose is to detect whether the marker leaked, not to validate a real token. Failure scenarios should cover at least a failed clone, a build command exiting, receipt of a termination signal, and repeated execution of the cleanup function.
When multiple teams share one physical node, the runner account should provide a second boundary. A temporary HOME addresses job-level residue, while separate accounts establish process, file-permission, and keychain boundaries between trust domains. Both layers are necessary to avoid treating script cleanup as the only line of defense.
The final acceptance criteria are straightforward: no credentials can be inherited before the job starts; credentials are provided only when needed during execution; every exit path triggers cleanup; and the next job cannot prove that the previous token ever existed. Only then do Git credentials truly belong to the job rather than to the long-running cloud Mac.
Frequently asked questions
Why is unsetting the token variable at the end of a job insufficient?
The credential may already have been persisted by a helper, embedded in a remote URL, written to global configuration, or stored in Keychain. Each location must be checked separately.
Should long-running Mac CI workers share the login Keychain?
Not across unrelated repositories or trust boundaries. Prefer short-lived, least-privilege tokens and separate runner users or dedicated keychains where persistent credentials are unavoidable.
Choose a dedicated physical machine for development, builds, and remote desktop access
Compare three Apple Silicon configurations and choose the node, rental term, and storage add-ons at checkout.