A credential committed to Git becomes part of a versioned object graph that collaborators and automation may copy. Removing the file from the latest tree does not remove the earlier commit containing it. The practical boundary is to keep credential values out of source history and provide them through an authorized runtime mechanism. Repository configuration can describe how to obtain a secret without containing the secret itself.
When it breaks
A clean checkout can conceal an exposed historical value. A new clone normally receives reachable history, including old versions of a deleted file. Git does not literally preserve every object forever under every cleanup policy, but deleting today’s file is not history cleanup. Rewriting one repository also cannot recall independent clones, forks, caches, or artifacts already distributed.
For a real exposure, first revoke or replace the affected credential and assess which systems accepted it. Removing a line changes publication, not authentication. Coordinate any history rewrite separately because it changes commit identities and can disrupt collaborators. Avoid reproducing the value in an issue, commit message, scanner report, or incident note while explaining what happened.
Runtime injection is not automatically confidential. Environment values can be inherited by child processes or captured by diagnostics; files can be read by identities that have sufficient access. Restrict the consuming service’s permissions, control the delivery channel, and keep secret values out of logs and generated output. A file outside the repository still needs an explicit owner and lifecycle.
Check it quickly
Run this Python script on a POSIX system with Git installed. It uses a plainly synthetic marker, never a working credential, and creates both repositories and runtime files inside one temporary directory. Inherited GIT_* overrides, system/global configuration, and template hooks are excluded from the fixture. The recorded run used Git 2.50.1:
from pathlib import Path
from tempfile import TemporaryDirectory
import io,os,stat,subprocess,sys,tarfile
env={key:value for key,value in os.environ.items() if not key.startswith('GIT_')}
env.update(GIT_CONFIG_NOSYSTEM='1',GIT_CONFIG_GLOBAL=os.devnull)
with TemporaryDirectory(prefix='credential-repo-lab-') as directory:
base=Path(directory);repo=base/'source';repo.mkdir()
def git(*args,cwd=repo,binary=False):
result=subprocess.run(['git',*args],cwd=cwd,env=env,capture_output=True,text=not binary)
assert result.returncode==0,result.stderr
return result.stdout
git('init','-q','--template=');git('config','user.name','Credential Lab')
git('config','user.email','credentials@example.invalid')
git('config','commit.gpgsign','false')
dummy='EXAMPLE-ONLY-NOT-A-REAL-SECRET\n'
(repo/'fixture.env').write_text(dummy)
(repo/'app.txt').write_text('read the configured credential file at runtime\n')
git('add','--','fixture.env','app.txt');git('commit','-qm','Synthetic exposure fixture')
original=git('rev-parse','HEAD').strip()
git('rm','--','fixture.env');git('commit','-qm','Remove fixture from current tree')
assert not (repo/'fixture.env').exists()
assert git('show',original+':fixture.env')==dummy
clone=base/'clone';git('clone','-q','--no-local',str(repo),str(clone))
assert not (clone/'fixture.env').exists()
assert git('show',original+':fixture.env',cwd=clone)==dummy
print('current file absent; historical fixture readable in source and clone')
archive=git('archive','--format=tar','HEAD',binary=True)
with tarfile.open(fileobj=io.BytesIO(archive),mode='r:') as bundle:
assert bundle.getnames()==['app.txt']
print('current-tree archive excludes fixture; Git history still contains it')
runtime=base/'runtime';runtime.mkdir(mode=0o700)
secret_file=runtime/'credential';secret_file.touch(mode=0o600)
secret_file.write_text(dummy)
assert stat.S_IMODE(secret_file.stat().st_mode)==0o600
child_env=dict(env,CREDENTIAL_FILE=str(secret_file))
child="from pathlib import Path; import os; p=Path(os.environ['CREDENTIAL_FILE']); assert p.read_text(); print('credential loaded without printing its value')"
result=subprocess.run([sys.executable,'-c',child],cwd=clone,env=child_env,text=True,capture_output=True)
assert result.returncode==0,result.stderr
assert dummy.strip() not in result.stdout+result.stderr
print(result.stdout.strip())
The first result confirms that the fixture is absent from both current working trees while its original content remains readable through the earlier commit in both repositories. The clone uses the normal Git transfer path rather than local hardlink optimization, so this checks a separately populated object database. The script compares the marker internally and does not print its value.
The current-tree archive contains only the application description. This proves that exporting one clean tree can exclude a deleted file while the repository history still contains it. It does not prove that every packaging method is safe: a build script may include other inputs, copy repository metadata, or embed a secret supplied during compilation.
The runtime example creates a private directory and a credential file with owner-only read and write permissions. A child process receives the file path through its environment, reads the file, and prints only confirmation. The assertion rejects accidental marker output. This demonstrates separation from the source tree and ordinary POSIX permissions, not a full secrets service, encrypted storage, or protection against the same operating-system identity.
Make delivery and recovery explicit
Commit a configuration schema or example containing names and harmless placeholders. Provision the actual value through the deployment platform or a secrets service with narrowly scoped access. Prefer an established workload identity when the platform supports it, so obtaining one credential does not require permanently distributing another powerful bootstrap secret. Decide how applications reload replacement values and how old access is revoked.
Place secret detection where it can prevent routine mistakes, such as before a commit and during CI, and verify it with appropriate synthetic fixtures. Pattern and entropy scanners can miss credentials they do not recognize and can also flag harmless examples. Treat their output as evidence to investigate, not proof that an unflagged repository is safe or permission to suppress broad categories indiscriminately.
Inspect actual release archives, container layers, build logs, caches, and diagnostic attachments according to the delivery path in use. A Git exclusion rule does not automatically govern those other outputs. Record affected credential identifiers and revocation evidence without copying the values. A successful repair combines invalidated access, controlled replacement, and understood distribution; a deletion commit alone establishes only that the latest tree changed.
Further reading
Sources
- GitHub sensitive-data removal, revocation before coordinated history cleanup and distributed-copy limits, checked 2026-09-12.
- OWASP secrets management, delivery, least privilege, logging, and credential lifecycle, checked 2026-09-12.
- git archive, exporting a selected repository tree, checked 2026-09-12.