Runtime Ambient Credential Hygiene
Gitmoot can curate the environment of runtime-agent subprocesses. The feature is
off by default and applies to Codex, Claude Code, Kimi Code, legacy kimi-cli,
and shell runtime adapters in foreground and daemon-worker delivery paths.
[credentials]
env_curation = true
env_passthrough = ["GOCACHE", "NPM_*"]
github = "deny"
model_gateway = false
model_gateway_allow_hosts = ["api.anthropic.com"]
# keychain_path = "/absolute/operator/path/keychain.env"
env_curation = false preserves the historical behavior: runtime commands
inherit the daemon or foreground shell environment, except for the Claude auth
overlay described below. Gitmoot reads this section when it constructs an
adapter for a job; it is not cached and needs no SIGHUP wiring.
Claude runtime auth
~/.gitmoot/runtime-auth.env is the authoritative Claude auth source and is
always mode 0600. Create or rotate it with gitmoot auth set claude; the
token is read from stdin and never appears in argv. Rotation is visible to the
next foreground or daemon delivery without a restart. gitmoot auth status
reports sources, masked fingerprints, mtime, and permissions without making a
runtime call. gitmoot auth probe claude performs a paid fresh-session live
check. gitmoot auth unset claude atomically writes an explicit empty file.
Managed names are CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY, and
ANTHROPIC_AUTH_TOKEN. When the file contains any one of them, Gitmoot injects
all three and blanks absent names so an ambient API key cannot override a
file-selected OAuth token. An explicit-empty file injects nothing and leaves
Claude's ambient/credential-store fallback intact. If the authoritative file is
missing, Gitmoot imports legacy daemon-runtime.env once, or otherwise seeds
from ambient managed variables once. Existing files are never overwritten.
For systemd deployments, keep daemon.env for operational values such as
PATH; do not duplicate Claude secrets there after bootstrap.
Key registry and grants
The shared key source is an operator-owned env file. Its default path
is <base-home>/.config/gitmoot/keychain.env; gitmoot key path prints the
resolved path and validation status. [credentials] keychain_path can select a
different absolute path. The file must be regular, owned by the current uid,
mode exactly 0600, and outside the Gitmoot home and managed checkouts. Gitmoot
revalidates and rereads it at every registration, preflight, and delivery, so an
atomic rewrite rotates values without a daemon restart.
The key commands manage names, modes, and grants only. They never accept or
print values:
gitmoot key path
gitmoot key add SOURCE_API_TOKEN --mode injected
gitmoot key grant SOURCE_API_TOKEN --pipeline nightly-sync
gitmoot key add PARTNER_API_TOKEN --mode proxied
gitmoot key configure PARTNER_API_TOKEN --upstream https://api.partner.example/v1 --auth bearer
gitmoot key grant PARTNER_API_TOKEN --agent researcher
gitmoot key list --json
gitmoot key show SOURCE_API_TOKEN
gitmoot key revoke SOURCE_API_TOKEN --pipeline nightly-sync
gitmoot key rm SOURCE_API_TOKEN --force
rm removes registry metadata and grants, not the operator's file entry.
proxied keys must be configured before grant with an absolute HTTPS upstream
and either bearer or approved custom-header placement. Pipeline shell stages
may request pipeline-granted injected or configured proxied names with
env_keys. Agent stages require both a configured proxied key granted to their
registered seat and an explicit stage selector; injected agent grants are
refused. Gates remain ineligible. Shell resolution includes the pipeline's own
file/defaults and pipeline grants, while agent resolution never does.
A proxied shell or agent stage receives a per-job placeholder in <KEY> and a loopback URL in
GITMOOT_PROXY_<KEY>_URL. Gitmoot pins the destination origin/base path,
rereads the keychain and rechecks the grant for every request, and revokes the
lease when delivery ends. The real value never enters an agent process. Proxied mode hides key bytes; it does not prevent
an authorized child from exercising the credential on the pinned upstream.
Curated upstreams and base paths are part of the model; configure only trusted
services.
Ordinary agent jobs receive no keys, and delegation children do not inherit a pipeline stage's access.
Claude model gateway
model_gateway = true opts Claude deliveries into a daemon-owned loopback
model gateway. The gateway listens only on an OS-assigned 127.0.0.1 port. For
each job, Gitmoot snapshots the selected real credential into daemon memory,
mints a random gitmoot-kc-... placeholder, and gives the Claude child only
that placeholder plus ANTHROPIC_BASE_URL pointing at the loopback listener.
The gateway authenticates the placeholder, replaces it with the real upstream
credential, streams the response, and revokes the placeholder when delivery
ends. Unknown and revoked placeholders receive 401.
Because Claude Code prefers a cached ~/.claude/.credentials.json over the
CLAUDE_CODE_OAUTH_TOKEN env var, the child is also pointed at a per-home
CLAUDE_CONFIG_DIR under the Gitmoot home that mirrors the operator's real
Claude config (settings, skills, commands, CLAUDE.md) but omits the cached
credential. Without this the child would authenticate from the cached
credential and ignore the placeholder — the gateway would then 401 the
delivery (#936). The mirror never contains a credential; the operator's real
config is only read, never modified.
Pipeline injected and generic proxied key delivery is independent of the
Claude gateway and does not alter its model-runtime placeholder flow.
The feature is off by default and currently covers Claude only. A populated
runtime-auth.env is required while it is enabled; Gitmoot fails the delivery
instead of falling back to ambient auth, Claude's credential store, or direct
upstream egress. model_gateway_allow_hosts is an exact hostname allowlist and
defaults to api.anthropic.com. The upstream is fixed by Gitmoot, not selected
by the child. Credentials are read once when the adapter is built, never once
per proxied request, so rotation applies to the next adapter/job.
Curated environment
When curation is enabled, Gitmoot forwards only these exact base names:
PATH,HOME,USER,LOGNAME,SHELLTMPDIR,TMP,TEMP,TZLANG,LANGUAGE,TERM,COLORTERM,NO_COLOR, plus everyLC_*nameXDG_CONFIG_HOME,XDG_CACHE_HOME,XDG_DATA_HOME,XDG_STATE_HOMEGOTOOLCHAINGIT_AUTHOR_NAME,GIT_AUTHOR_EMAIL,GIT_COMMITTER_NAME,GIT_COMMITTER_EMAILGITMOOT_HOME
Runtime-specific additions are:
- Codex:
CODEX_HOME - Claude Code:
CLAUDE_CONFIG_DIR; the managed auth overlay is appended fromruntime-auth.envafter the curated base. - Kimi Code, legacy
kimi-cli, and shell: no additions. Shell stage variables, chat-relay variables, pipeline metadata, and the upstream-context file variable are job-owned injections and are appended after the curated base.
SSH_AUTH_SOCK, GH_*, GITHUB_*, proxy variables, and toolchain cache
variables such as GOCACHE are not in the base list. Add non-secret operational
variables with env_passthrough. Each entry is an exact name or a single
trailing-* prefix glob, such as GOCACHE or NPM_*. Names containing = or
NUL and non-trailing * forms are rejected. A bare * entry is legal and
passes every ambient variable through (except denied GitHub credentials) --
that keeps GitHub denial while giving up the rest of the curation boundary,
so treat it as a deliberate escape hatch, not a default.
GitHub denial and opt-out
With curation on, github = "deny" is the default. Ambient GH_* and
GITHUB_* values, including GH_TOKEN, GITHUB_TOKEN,
GH_ENTERPRISE_TOKEN, and GITHUB_ENTERPRISE_TOKEN, are omitted even if an
env_passthrough glob would otherwise match them. Gitmoot injects
GH_PROMPT_DISABLED=1 and points GH_CONFIG_DIR at a fresh, empty, delivery
scratch directory with mode 0700. The directory is removed when the runtime
subprocess completes on success, failure, timeout, or cancellation.
Set github = "inherit" as an explicit opt-out. Gitmoot then forwards ambient
GH_* and GITHUB_* variables and does not inject a GitHub config directory or
disable prompts.
Limits
The model gateway is credential custody, policy, and attribution, not a hard egress boundary. These two limits are deliberate:
- env-var routing is cooperative, not a hard egress boundary — a malicious
agent can unset
ANTHROPIC_BASE_URL, or pointCLAUDE_CONFIG_DIRback at the operator's real~/.claudeto read the cached credential directly. This buys credential custody/policy/attribution against a prompt-injected or misbehaving agent, not enforcement against one that actively evades. - The strong "agents never hold real credentials" claim also requires
Landlock read-rules for
runtime-auth.envand~/.claude/.credentials.json(same-UID read is currently possible) — that is P3.
Codex/Kimi custody, MITM CA support, corporate proxy/NO_PROXY interoperability,
Landlock read restrictions, and hard egress enforcement remain P3. SSH keys,
SSH agents, Git credential helpers, and direct network access are untouched.