# 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. 1. Open **Packages → CLI Credentials**. 2. Pick the `git` row and open **Agent Access**. 3. Use the **Credential** tab to select the agent. 4. Choose **Credential Type**: `Personal Access Token` or `SSH Private Key`. 5. 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. 6. Paste the token (PAT) or the unencrypted PEM body (SSH). 7. Save. Use the **Access policy** tab in the same Agent Access dialog when you need to change deny args, timeout, tips, or env overrides for that agent. Agent Access is one dialog on purpose: policy and secret storage stay separate internally, but operators should manage them as one access decision. ## 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. 1. Open **Packages → CLI Credentials → Advanced User Overrides → Add**. 2. Select user. 3. Choose **Credential Type**: `Personal Access Token` or `SSH Private Key`. 4. 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. 5. Paste the token (PAT) or the unencrypted PEM body (SSH). 6. 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: 1. User override. 2. Channel/context credential. 3. Agent credential. 4. Binary-level env defaults. ## What gets auto-injected The adapter runs ONLY for these subcommands: - `clone` - `fetch` - `pull` - `push` - `submodule` 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 typed PAT/SSH credential is selected, or the selected credential cannot match the resolved remote host, adapter-managed remote commands fail closed with a GoClaw diagnostic. `git` is not allowed to fall through to an interactive username/password prompt in agent runtime. ## 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..extraheader` config entry with `Authorization: Basic base64("x-access-token:")`. - **The PAT never appears on argv** — so `ps`, `/proc//cmdline`, and shell-history echoes don't expose it. - The raw PAT, base64 payload, and full injected header are all registered with the scrubber before tool output is returned to the agent. - The injected env vars are scoped to the spawned `git` process 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 in `os.TempDir()` (per-user on POSIX) with a `goclaw-gitkey-*` prefix. - `GIT_SSH_COMMAND` is set to `ssh -i -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new`. - The tmpfile is removed via `defer` on the exec wrapper. **SIGKILL of goclaw leaves the file orphaned** — see the Operator Notes section below. - SSH private keys are validated twice at save time: first with Go's SSH parser, then with OpenSSH via `ssh-keygen -y -f ` when `ssh-keygen` is available. This catches keys that would otherwise save successfully but fail later with OpenSSH diagnostics such as `error in libcrypto`. - `StrictHostKeyChecking=accept-new` accepts 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`: ```sh ssh-keyscan github.com >> ~/.ssh/known_hosts ``` v2 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.Content` returned 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= 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: ```sh 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: ```sh 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 by `TestEmitSystemEnvInjectionAudit_*` — 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_hosts` per 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`/`gcloud` to non-passthrough adapters as use cases arise (e.g. `aws assume-role` needs argv mutation). - v2: Dedicated `audit_log` table for `security.system_env_injection` events.