You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Scope the process-global environment mutation in run_task (git identity + gh tokens) to the
lifetime of the call, so it cannot leak into a long-lived in-process caller.
Background
#623 fixed the
destructive git config --global clobber (#622) by switching to GIT_AUTHOR_* / GIT_COMMITTER_*
environment variables. That is the correct mechanism and is strictly better than the
predecessor — the old code persisted identity to ~/.gitconfig and survived reboots; the new code
only persists for the lifetime of the Python process.
The variables are set via direct os.environ[...] writes in run_task:
# Set env vars for the prepare-commit-msg hook BEFORE setup_repo()
# so the hook has access to TASK_ID/PROMPT_VERSION from the start.
os.environ["TASK_ID"] =config.task_id
ifprompt_version:
os.environ["PROMPT_VERSION"] =prompt_version
Because these are process-global, they persist after run_task returns for as long as the calling
process lives. This was raised as a non-blocking known-property note in the #623 review: #623 (comment)
The residual edge (not a bug today)
No current caller triggers this — the AgentCore container runs run_task then exits, and the
dev-workstation path (#622's motivation) is a short-lived script that also exits. The latent edge is
a long-lived in-process caller that runs its own git commitafterrun_task in the same
process without setting identity: that commit would be silently attributed to bgagent.
The same block also writes GITHUB_TOKEN / GH_TOKEN to os.environ (pipeline.py:835-836), so
this is token hygiene as well as commit-attribution correctness — a lingering token in a
long-lived process is the more security-relevant leak of the two.
Proposed fix (option B — scope, don't eliminate)
Keep the current mechanism (subprocesses — the Claude Code CLI spawn and the post_hooks.py
safety-net commit — inherit identity via os.environ, which is exactly why the env-var approach
works), but bound its lifetime with a try/finally restore. Because every commit happens within run_task's scope, restoring the prior environment at exit is safe — all commits are already done
by then.
A small context manager keeps it clean:
fromcontextlibimportcontextmanager@contextmanagerdef_scoped_environ(**overrides: str):
"""Set env vars for the duration of the block, restoring prior values (or deleting keys that were previously unset) on exit — even on exception."""prior= {k: os.environ.get(k) forkinoverrides}
os.environ.update(overrides)
try:
yieldfinally:
fork, oldinprior.items():
ifoldisNone:
os.environ.pop(k, None)
else:
os.environ[k] =old
Then wrap the identity/token writes (and the downstream repo setup + agent run that depend on them)
in with _scoped_environ(GIT_AUTHOR_NAME="bgagent", ...): ... inside run_task.
Why B and not the alternatives
Document-only: acceptable but leaves the token-lingering property in place.
Eliminate via explicit env=: threading an explicit env dict into every subprocess
(setup_repo, the CLI spawn, post_hooks) is fully correct but a cross-call-site refactor that
is easy to get wrong (the CLI spawn inherits os.environ today; shell.py._clean_env() strips
only OTEL_* / PYTHONPATH). Not worth it for a currently-unreachable edge.
B neutralizes both the misattribution and the token-lingering edges in one localized change
with no call-site threading, and it is the production mirror of what the test already does — the
new regression test uses monkeypatch.delenv(..., raising=False) to restore env on teardown
(agent/tests/test_pipeline.py:166). B gives production the same lifecycle the test assumes.
Acceptance criteria
run_task restores (or deletes, if previously unset) GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL, GITHUB_TOKEN, GH_TOKEN, TASK_ID, PROMPT_VERSION on exit — on both the return path and the exception path.
Summary
Scope the process-global environment mutation in
run_task(git identity +ghtokens) to thelifetime of the call, so it cannot leak into a long-lived in-process caller.
Background
#623 fixed the
destructive
git config --globalclobber (#622) by switching toGIT_AUTHOR_*/GIT_COMMITTER_*environment variables. That is the correct mechanism and is strictly better than the
predecessor — the old code persisted identity to
~/.gitconfigand survived reboots; the new codeonly persists for the lifetime of the Python process.
The variables are set via direct
os.environ[...]writes inrun_task:sample-autonomous-cloud-coding-agents/agent/src/pipeline.py
Lines 824 to 842 in 5747dbd
Because these are process-global, they persist after
run_taskreturns for as long as the callingprocess lives. This was raised as a non-blocking known-property note in the #623 review:
#623 (comment)
The residual edge (not a bug today)
No current caller triggers this — the AgentCore container runs
run_taskthen exits, and thedev-workstation path (#622's motivation) is a short-lived script that also exits. The latent edge is
a long-lived in-process caller that runs its own
git commitafterrun_taskin the sameprocess without setting identity: that commit would be silently attributed to
bgagent.The same block also writes
GITHUB_TOKEN/GH_TOKENtoos.environ(pipeline.py:835-836), sothis is token hygiene as well as commit-attribution correctness — a lingering token in a
long-lived process is the more security-relevant leak of the two.
Proposed fix (option B — scope, don't eliminate)
Keep the current mechanism (subprocesses — the Claude Code CLI spawn and the
post_hooks.pysafety-net commit — inherit identity via
os.environ, which is exactly why the env-var approachworks), but bound its lifetime with a
try/finallyrestore. Because every commit happens withinrun_task's scope, restoring the prior environment at exit is safe — all commits are already doneby then.
A small context manager keeps it clean:
Then wrap the identity/token writes (and the downstream repo setup + agent run that depend on them)
in
with _scoped_environ(GIT_AUTHOR_NAME="bgagent", ...): ...insiderun_task.Why B and not the alternatives
env=: threading an explicit env dict into every subprocess(
setup_repo, the CLI spawn,post_hooks) is fully correct but a cross-call-site refactor thatis easy to get wrong (the CLI spawn inherits
os.environtoday;shell.py._clean_env()stripsonly
OTEL_*/PYTHONPATH). Not worth it for a currently-unreachable edge.with no call-site threading, and it is the production mirror of what the test already does — the
new regression test uses
monkeypatch.delenv(..., raising=False)to restore env on teardown(
agent/tests/test_pipeline.py:166). B gives production the same lifecycle the test assumes.Acceptance criteria
run_taskrestores (or deletes, if previously unset)GIT_AUTHOR_NAME,GIT_AUTHOR_EMAIL,GIT_COMMITTER_NAME,GIT_COMMITTER_EMAIL,GITHUB_TOKEN,GH_TOKEN,TASK_ID,PROMPT_VERSIONon exit — on both the return path and the exception path.post_hooks.pysafety-net commit) stillresolve identity to
bgagent <bgagent@noreply.github.com>(no regression to fix(agent): stop clobbering developer ~/.gitconfig — use GIT_AUTHOR/COMMITTER env vars instead ofgit config --global#622's fix).run_taskreturns, the six vars hold theirpre-call values (unset stays unset) in the calling process.
test_pipeline.pyidentity assertions still pass.References
git config --global(#622) #623 (which fixed fix(agent): stop clobbering developer ~/.gitconfig — use GIT_AUTHOR/COMMITTER env vars instead ofgit config --global#622).git config --global(#622) #623 (comment)