9.5 KiB
Git Credential Adapter
User-facing guide for the git typed credential adapter (issue #82, ships with
v3.x).
Why a typed adapter?
The legacy CLI credential flow asks the user to paste arbitrary environment
variables (GH_TOKEN, KUBECONFIG, etc.). git does not read its
authentication from a stable, single env var — credentials live in
.git/config, ~/.git-credentials, the OS credential helper, or per-remote
URLs. Pasting a PAT into GIT_TOKEN did nothing, which surprised users and
silently failed every clone.
The typed git adapter accepts either a Personal Access Token (PAT) or an
SSH private key, validates it server-side, then injects it into the spawned
git process via a transient mechanism that never touches disk or argv.
When to use which credential type
| Type | Use when | Limits |
|---|---|---|
| PAT | GitHub/GitLab/Gitea over HTTPS. You already have a ghp_… or glpat-… token. |
Token must be unscoped to specific repos, OR cover all repos goclaw will touch. |
| SSH | Self-hosted git over SSH. You manage ~/.ssh/known_hosts or accept TOFU risk. |
Passphrase-protected keys are NOT supported (see below). |
| Env | Legacy path — you have a custom env-var-driven workflow. | Loses host-scoped routing; same trust profile as other CLIs. |
Adding an agent credential (UI)
Agent credentials are the default path for git auth. They avoid channel-user ID ambiguity: the selected agent owns the credential, and anyone allowed to use that agent can cause it to run git with the stored credential.
- Open Packages → CLI Credentials.
- Pick the
gitrow and open Agent Credentials. - Select the agent.
- Choose Credential Type:
Personal Access TokenorSSH Private Key. - Enter Host Scope (required for PAT/SSH): the hostname the credential
authenticates to.
- Examples:
github.com,gitlab.example.com,gitea.internal:8443. - Case-insensitive. Punycode normalized via
idna.ToASCII. - Port included only when non-default for the scheme.
- Examples:
- Paste the token (PAT) or the unencrypted PEM body (SSH).
- Save.
Advanced user overrides
Per-user credentials remain available for personal overrides and backward compatibility. Use them only when a stable tenant user ID is the intended credential boundary.
- Open Packages → CLI Credentials → Advanced User Overrides → Add.
- Select user.
- Choose Credential Type:
Personal Access TokenorSSH Private Key. - Enter Host Scope (required for PAT/SSH): the hostname the credential
authenticates to.
- Examples:
github.com,gitlab.example.com,gitea.internal:8443. - Case-insensitive. Punycode normalized via
idna.ToASCII. - Port included only when non-default for the scheme.
- Examples:
- Paste the token (PAT) or the unencrypted PEM body (SSH).
- Save.
The stored secret is encrypted (AES-256-GCM) and can never be read back through
the API or UI. Editing the row shows a •••••••• placeholder; leaving the
secret field blank preserves the stored value, typing a new value replaces it.
Effective credential precedence is:
- User override.
- Channel/context credential.
- Agent credential.
- Binary-level env defaults.
What gets auto-injected
The adapter runs ONLY for these subcommands:
clonefetchpullpushsubmodule
Any other subcommand (status, log, diff, commit, branch, etc.) runs
WITHOUT credentials — these are local operations and never reach a remote.
Implementation: see internal/tools/credential_adapter_git.go::ShouldInject.
Host-scope semantics
host_scope is the exact ASCII hostname (with optional port) the
credential is valid for. v1 does NOT support wildcards.
Stored github.com matches:
- ✓
git clone https://github.com/org/repo.git - ✗
git clone https://api.github.com/...(different host) - ✗
git clone https://github.com:8443/...(different port — port is part of the scope key)
Stored gitea.example.com:8443 matches:
- ✓
git clone https://gitea.example.com:8443/... - ✗
git clone https://gitea.example.com/...(default port — still mismatch)
If you run a self-hosted server on the scheme's default port (443 HTTPS, 22 SSH), omit the port. If you run on a non-default port, include it.
When no stored credential matches the resolved remote host, the adapter falls
through to the un-credentialed path: git runs with whatever credentials the
calling shell already has (typically none, in which case the remote will reject
the operation with a 401/403).
Security model
PAT path
- Injected via
GIT_CONFIG_COUNT+GIT_CONFIG_KEY_*/GIT_CONFIG_VALUE_*environment variables. - The PAT itself goes into a value that synthesizes an
http.<remote>.extraheaderconfig entry withAUTHORIZATION: basic <base64(token)>. - The PAT never appears on argv — so
ps,/proc/<pid>/cmdline, and shell-history echoes don't expose it. - The injected env vars are scoped to the spawned
gitprocess only; they are NOT inherited by goclaw, by other tools, or by sibling exec calls.
SSH path
-
The PEM key is written to an
0600-mode tmpfile inos.TempDir()(per-user on POSIX) with agoclaw-gitkey-*prefix. -
GIT_SSH_COMMANDis set tossh -i <tmpfile> -F /dev/null -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new. -
The tmpfile is removed via
deferon the exec wrapper. SIGKILL of goclaw leaves the file orphaned — see the Operator Notes section below. -
StrictHostKeyChecking=accept-newaccepts unknown host keys on first contact (TOFU). A network attacker positioned between goclaw and the git host CAN capture the SSH session on the first connection. Operators should pre-seed~/.ssh/known_hosts:ssh-keyscan github.com >> ~/.ssh/known_hostsv2 will support per-credential pinned host keys.
Passphrase-protected SSH keys: rejected
The adapter rejects encrypted SSH keys at validation time with error_key = git.cred_ssh_passphrase_unsupported. Reason: we have no UX or storage slot
for the passphrase, and ssh-agent forwarding is outside the goclaw security
model. Re-export your key without a passphrase, or use a dedicated deploy key.
Redaction across output channels
Every credential adapter registers its secret bytes with the per-request
ScrubCredentials bag (internal/tools/scrub.go). The scrubber removes the
secret from:
- Live stdout / stderr streamed to the agent.
- The final
Result.Contentreturned by the tool. - Error messages bubbled up to the agent.
- The audit log line (
security.system_env_injection) — see below.
Plaintext hostnames are also kept out of the audit log: host_scope_hash is
the SHA-256 first 8 hex chars of the normalized scope.
Auditability
Every successful credential injection emits exactly one structured log line:
level=WARN msg=security.system_env_injection
adapter=git binary=git user_id=<uuid> credential_source=agent
env_keys=[GIT_CONFIG_COUNT,GIT_CONFIG_KEY_0,GIT_CONFIG_VALUE_0]
argv_prefix_len=0
host_scope_hash=3aeb0024
env_keys lists NAMES only — values never appear. host_scope_hash is the
first 8 hex chars of sha256(normalized_host_scope). Operators wanting to
grep for activity against a specific host pre-compute the hash:
echo -n "github.com" | sha256sum | cut -c1-8
See docs/09-security.md → "CLI credential adapters" for the full schema.
Migration from legacy env-paste
Existing rows in secure_cli_user_credentials with credential_type IS NULL
or = 'env' continue to work via the passthrough adapter. Existing user
overrides remain higher precedence than agent credentials. There is no forced
migration.
To move to the agent-scoped model, create a matching Agent Credential for the agent and remove the user override when the override is no longer needed.
Operator notes
-
Tmpfile sweep: high-security deployments should sweep stale tmpfiles every few minutes:
find "$TMPDIR" -name 'goclaw-gitkey-*' -mmin +60 -delete find "$TMPDIR" -name 'goclaw-pgpass-*' -mmin +60 -delete -
Pre-seed known_hosts to defeat TOFU MITM (see SSH path above).
-
Log aggregation: route
security.*slog events to your SIEM. The schema is pinned byTestEmitSystemEnvInjectionAudit_*— alert on any change. -
No sandbox support v1: the adapter mutates the parent process's forked-child environment, which is incompatible with the bind-mount-based sandbox path. Sandbox + credentialed exec is on the v2 roadmap.
Known limitations (v1)
- One credential per (agent, binary) row, plus legacy one credential per (user, binary) override.
- No multi-host wildcard (
*.github.com). - No passphrase-protected SSH keys.
- No persistent
known_hostsper credential (TOFU only). - No sandbox support.
- PAT scope cannot be inspected — goclaw stores the token opaquely.
Future work
Tracked separately:
- v2: OAuth device-flow for GitHub/GitLab — eliminates PAT paste.
- v2: Multi-credential per user with host routing logic.
- v2: Sandbox/Docker exec path support (per-call key bind-mount).
- v2: Pinned SSH host keys per credential.
- v2: Migrate
gh/aws/gcloudto non-passthrough adapters as use cases arise (e.g.aws assume-roleneeds argv mutation). - v2: Dedicated
audit_logtable forsecurity.system_env_injectionevents.