Access token is written to logs and to RuntimeError messages when cloning with a token (0.3.1)
Summary
When gitingest clones a private repository with a token, the credential reaches two outputs that callers commonly capture: the INFO log line, and the message of the RuntimeError raised on clone failure. The redaction that exists targets the URL, while the credential lives in the -c http.<url>.extraheader=Authorization: Basic <base64> argument that precedes it.
Measured on 0.3.1, Python 3.13, Windows.
Where it comes from
gitingest/clone.py builds the command as:
clone_cmd = ["git"]
if token and is_github_host(url):
clone_cmd += ["-c", create_git_auth_header(token, url=url)]
clone_cmd += ["clone", "--single-branch", "--no-checkout", "--depth=1"]
...
clone_cmd += [url, local_path]
logger.info("Executing git clone command",
extra={"command": " ".join([*clone_cmd[:-1], "<url>", local_path])})Two consequences:
The redaction misses its target.
clone_cmd[:-1]drops only the last element (local_path), so the auth header at index 2 is printed verbatim, and the URL — the thing<url>is meant to stand for — is still inside the slice and printed too. The line reads as redacted while carrying the credential.RuntimeErrorcarries the same string.run_commandingitingest/utils/git_utils.pyraises:msg = f"Command failed: {' '.join(args)}\nError: {stderr.decode().strip()}" raise RuntimeError(msg)argsis the fullclone_cmd, header included. Any caller that stores or reportsstr(exc)— an API response, a job record, an agent transcript — persists the credential. Silencing the logger does not cover this path.
The header value is base64 of x-oauth-basic:<token>, so it is reversible, not a hash.
Reproduce
import asyncio, gitingest
asyncio.run(gitingest.ingest_async("https://github.com/<owner>/<private-repo>", token="<a PAT>"))with logging at INFO. The Executing git clone command line contains the header. For path 2, point at a repository the token cannot reach: the RuntimeError message contains it as well.
Suggested fix
Redact by value, at both sites, rather than by position:
- build the log/error string from a sanitised copy of
clone_cmdwhere any element matchingextraheader=Authorization:(and anyhttps://user:pass@form) is replaced; - apply the same sanitiser in
run_commandbefore interpolatingargsintomsg, so the exception path is covered independently of the logging path.
Doing it in run_command alone would cover both, since that is where the argument list becomes text a caller can keep.
Impact
A token with repo scope can leak into logs and error stores of any downstream consumer, including ones that never touch gitingest internals. We hit it in a downstream project and had to add both a logger mute and an output scrubber; the second was only found because a real run surfaced the exception path after the logger was already silenced.
Happy to open a PR if the shape above matches what you'd want.
Source: coderamp-labs/gitingest