---
title: Authenticate clone, fetch and push
description: How to attach a credential to a git operation, where the secret goes, and what to check when authentication fails.
tags: [how-to, git, credentials, security]
---
# Authenticate clone, fetch and push
Three `Repo` operations can authenticate: `clone`, `fetch` and `push`. Each
takes an options struct with an optional `CredentialRef`.
## Attach a credential
```rust
use rtb_credentials::CredentialRef;
use rtb_forge::git::{CloneOptions, Repo};
let cref = CredentialRef {
env: Some("GITLAB_TOKEN".into()),
..Default::default()
};
let repo = Repo::clone(
"https://gitlab.com/phpboyscout/rust/forge.git",
"/tmp/forge",
CloneOptions::default().with_credential(cref),
).await?;
```
`FetchOptions` and `PushOptions` work identically:
```rust
use rtb_forge::git::{FetchOptions, PushOptions};
repo.fetch("origin", FetchOptions::default().with_credential(cref.clone())).await?;
repo.push("origin", "HEAD:refs/heads/topic", PushOptions::default().with_credential(cref)).await?;
```
Omit `with_credential` and the operation runs anonymously.
## Choose where the secret comes from
`CredentialRef` is `rtb-credentials`' declarative reference. It is resolved
through a fixed precedence chain (environment variable, then OS keychain, then
a literal in config, then a fallback environment variable) and the first one
that produces a value wins.
```rust
use rtb_credentials::{CredentialRef, KeychainRef};
// From an environment variable.
CredentialRef { env: Some("GITLAB_TOKEN".into()), ..Default::default() }
// From the OS keychain.
CredentialRef {
keychain: Some(KeychainRef { service: "mytool".into(), account: "gitlab".into() }),
..Default::default()
}
// Environment variable, falling back to an ecosystem default.
CredentialRef {
env: Some("MYTOOL_GITLAB_TOKEN".into()),
fallback_env: Some("GITLAB_TOKEN".into()),
..Default::default()
}
```
`CredentialRef` derives `Deserialize`, so the usual arrangement is to carry it
in your tool's own config and let the user decide:
```yaml
forge:
credential:
env: GITLAB_TOKEN
```
A literal secret in config is refused when the process runs with `CI=true`.
That is `rtb-credentials`' rule, not this crate's.
## Understand where the secret ends up
`rtb-forge` resolves the credential to a `SecretString`, then runs `git` with:
- the secret in the child process's environment as `RTB_VCS_GIT_TOKEN`;
- `-c credential.helper='!f() { echo username=x-access-token; echo password=$RTB_VCS_GIT_TOKEN; }; f'`
in argv, a script that contains no secret, only the variable name;
- `GIT_TERMINAL_PROMPT=0`, so an auth failure exits rather than prompting.
The secret is never in argv, never in a temporary file, and never written to
`~/.git-credentials` or any repository config. It is not logged: error `cause`
strings carry git's stderr, which does not include the token.
The variable name still says `VCS` because the crate was renamed from
`rtb-vcs`; the name is part of the runtime contract and was deliberately not
changed. There is no `RTB_FORGE_GIT_TOKEN`.
## Know the constraints before you debug
**HTTPS only.** The credential helper mechanism applies to HTTPS remotes. An
`ssh://` or `git@host:` remote authenticates through the SSH agent as usual and
ignores the `CredentialRef` entirely.
**The username is fixed.** It is always `x-access-token`, GitHub's PAT
convention, which GitLab and Gitea also accept. There is no way to change it.
A forge requiring a real username alongside a token cannot be authenticated
through these methods; run `git` yourself for that case.
**Authenticated clone needs the `git` binary.** An anonymous clone runs on
`gix` and needs nothing external. Adding a credential switches it to a `git`
subprocess. On a machine without `git` on `PATH`, the anonymous clone succeeds
and the authenticated one fails. See
[Why the write paths shell out to git](../explanation/03-why-write-paths-shell-out.md).
## Tell the two failure modes apart
They are different variants and they mean different things:
```rust
use rtb_forge::git::RepoError;
match repo.fetch("origin", opts).await {
Err(RepoError::Auth(source)) => {
// The credential could not be RESOLVED locally.
// Env var unset, keychain locked, literal refused under CI=true.
eprintln!("credential problem: {source}");
}
Err(RepoError::FetchFailed { remote, cause }) => {
// The credential was resolved and the REMOTE rejected it,
// or the network failed. `cause` is git's stderr.
eprintln!("fetch from {remote} failed: {cause}");
}
Err(other) => return Err(other.into()),
Ok(()) => {}
}
```
`RepoError::Auth` never means "the server said no". If you see it, check the
environment variable name and the keychain entry, not the token's scopes.
## Reuse one credential across operations
`CredentialRef` is `Clone`, and resolution happens per call. The secret is not
cached between operations. Clone the reference, not the secret:
```rust
let cref = CredentialRef { env: Some("GITLAB_TOKEN".into()), ..Default::default() };
let repo = Repo::clone(url, dst, CloneOptions::default().with_credential(cref.clone())).await?;
repo.fetch("origin", FetchOptions::default().with_credential(cref.clone())).await?;
repo.push("origin", "main", PushOptions::default().with_credential(cref)).await?;
```
Each call re-walks the precedence chain, so a token rotated in the keychain
between calls takes effect immediately.