Files
skill-git-push/skills/updating-gitea-repositories/references/credential-persistence.md
T

12 KiB

Credential Persistence and Safe Diagnosis

Scope and inspection

Work on the machine and OS user running Git. First identify a clean HTTPS site origin, the intended Gitea account, the repository remote, and any explicit account/path isolation. Do not print a remote URL before checking it for user information. Treat helper command strings and Git configuration values as potentially sensitive; capture and sanitize them before reporting.

Inspect system, user, repository, worktree, command-line, included, and URL-scoped configuration. credential.helper can be multivalued: an empty value resets preceding helpers, later matching values can add helpers again, and --get-urlmatch credential.helper alone does not show the full effective chain. Check credential.useHttpPath, credential.username, credential.provider, credential.credentialStore, and credential.namespace as applicable. Respect intentional per-repository account overrides and URL paths.

For one account shared by repositories on the same site, a site-scoped credential.useHttpPath=false allows host-level matching. Do not force this when repository-specific credentials or another account are intentionally isolated. Confirm the effective helper uses a usable persistent store; never silently replace it with plaintext or a short-lived cache. A site-scoped empty helper followed by a secure helper can override earlier helpers, but later or more-specific configuration can change the result. A later repository-local generic helper reset can also replace the user-level site chain. Verify the complete effective sequence in each affected repository before using it. Preserve other sites and existing vault entries.

When the task authorizes configuration changes, adjust only the necessary credential.<https-site>.* keys in the current user's Git configuration. Preserve existing nonconflicting settings. If changes would replace an ambiguous account, site-specific setting, or credential, stop and resolve the ambiguity. Do not run a global GCM configure command as a blind repair: it can change unrelated helper configuration.

Reuse and diagnosis

Try the matching existing secure credential before asking the user to create or enter a PAT. When the account and site are known and the existing credential is a PAT, this bundled script checks a Gitea API identity endpoint and Git read advertisement without calling Git's credential reject or erase:

python <skill-directory>/scripts/credential_probe.py --repo <repository-directory> --site https://gitea.example.com --account <confirmed-account>

On Windows use py -3 if python is unavailable. The program accepts a clean origin and a same-site HTTPS .git remote, never follows redirects, and captures helper output internally. It does not support Gitea installations under a URL subpath or OAuth refresh. A matching API identity establishes the returned account when the token has the required API scope. A successful Git read does not prove push permission; a public repository may permit anonymous reads. API denial can also reflect insufficient API scope. HTTP 401 establishes rejection for that request, not why it was rejected. HTTP 403, 404, TLS failures, and network errors likewise need their own diagnosis. Do not repeatedly retry rejected credentials.

Normal Git authentication can send erase to every active helper after a 401, even when the intent was only to inspect a remote. Do not run a normal failing fetch, ls-remote, or push against an original credential that must be preserved. The probe uses direct read-only HTTP requests; use another independently verified get-only helper for cases it cannot handle. Never display git credential fill output, enable secret tracing, or run git credential reject as a probe.

Copy a known old credential

When a saved entry's site and account are unambiguous, Windows can copy one git-credential-store entry into a newly generated Windows Credential Manager namespace without displaying it:

py -3 <skill-directory>/scripts/credential_migrate.py --site https://gitea.example.com --account <confirmed-account> --source-file <existing-credential-store-file>

Inspect which helper and file are actually in use first; specify that file explicitly. The script refuses multiple matching entries or path-scoped entries. The script selects one helper, validates the returned account, refuses a populated new namespace, copies via pipes, verifies secure-store readback, and does not remove the source or change Git configuration. The saved username is not proof that the token belongs to that account; validate it with the server. The old plaintext file remains on disk: tell the user. The program does not migrate OAuth refresh tokens or path-specific selections, and automatic migration to macOS or Linux stores is not provided; use a separately verified safe mechanism or a local login.

To verify the copy before using it, run the PAT probe with GCM_NAMESPACE set to the non-secret namespace the script printed. Only after a successful and unambiguous verification, update the target site's credential.namespace and other necessary site-scoped settings. Preserve the old namespace and credential. If validation is inconclusive or fails, leave existing Git settings unchanged. A secret is never an environment variable; the namespace is only a non-secret identifier.

First login and renewal

If the credential is absent, rejected after configuration and account checks, or authorization is genuinely required, use a supported browser OAuth flow when appropriate, or ask the user to authenticate in a trusted local GCM prompt. An existing valid PAT can be entered; create a new least-privilege PAT only if needed. Do not claim an existing token is expired or revoked based only on 401. To preserve a rejected old entry, use a fresh isolated namespace for the interactive attempt, verify it, then configure that namespace for the site. Do not replace or delete the old record as a side effect.

Browser OAuth for OIDC accounts

Gitea web SSO and Git OAuth are distinct. An OIDC-only account does not need a local Gitea password: the user can authorize a Git OAuth application through their usual browser login. Check that the site supports the registered public client, PKCE, a browser callback, and refresh tokens; an administrator may disable Gitea's preconfigured applications. An initial OIDC sign-in can land on the Gitea home page instead of returning to the authorization request. If that happens, restart the OAuth request while the browser session is signed in. Use a suitable PAT only when the OAuth path cannot be used.

On native Windows, an option verified with Git 2.52 and git-credential-oauth 0.17.2 uses the official OAuth helper for browser authorization and the bundled Windows vault helper for subsequent Git requests. Git 2.45 or later is recommended for refresh-token protocol support. Install the official OAuth helper when needed and authorized. The following tool opens the browser, checks the returned Gitea account and Git read advertisement, then saves a refreshable credential to a newly generated isolated Windows vault namespace. It never changes Git configuration or displays tokens. A public repository's read advertisement alone does not prove that Git accepted the credential or that the user can push:

py -3 <skill-directory>/scripts/credential_oauth_bootstrap.py --site https://gitea.example.com --account <confirmed-account> --repo <repository-directory>

The default client is Gitea's preconfigured public git-credential-oauth application; use --client-id for another compatible public client. This bootstrap handles site-root HTTPS installations. It requests read:user and write:repository: the latter can include repository API changes and deletion as well as Git writes, within the user's existing permissions. Explain the scope when the user authorizes. The API identity scope is needed for account verification. If the server, scope policy, network, or client does not support this flow, diagnose the actual response and retain the PAT path. Never configure a client secret in Git.

The script prints non-secret metadata, including the isolated namespace, verified account, public client ID, installed paths, and a shell-escaped helper_command. Configure the target site's effective helper sequence using Git config commands or an included file:

  1. Reset preceding helpers with an empty credential.<site>.helper.
  2. Add the exact helper_command from the bootstrap metadata as the sole helper after that reset. Pass it to Git as one argument, for example through a subprocess argument list; do not manually build a shell command with double-quoted paths, since $() and backticks can still be evaluated. The command uses POSIX shell quoting for each argument. The installed, content-addressed vault helper lives under the user's local application data, not the transient skill cache.
  3. Set site-specific credential.username=oauth2, credential.useHttpPath=false, credential.oauthClientId to the metadata's public client ID, credential.oauthAuthURL=/login/oauth/authorize, credential.oauthTokenURL=/login/oauth/access_token, and credential.oauthScopes=read:user write:repository.
  4. Check that no later generic or repository-local helpers add a plaintext store or reset this sequence. When a repository-local override is necessary, add the same site-specific reset and helper there without removing unrelated settings. Verify from each actual Git environment without exposing git credential fill output.

oauth2 is the Git protocol username, not proof of the user's identity. In a later session, safely query the current OAuth account before reuse:

py -3 <skill-directory>/scripts/credential_oauth_identity.py --site https://gitea.example.com --namespace <namespace> --repo <repository-directory>

The identity check can refresh and persist an expired credential; it prints only the account name. Do not use a shared host-level setup when multiple accounts for the same site cannot be unambiguously isolated. An empty namespace fails closed; only the bootstrap may create a verified entry.

The vault helper stores access/refresh/expiry as a compressed Windows vault blob. Under a cross-process lock it refreshes an expired or rejected token, saves the rotated refresh token before returning access to Git, and ignores stale store calls. A matching automatic erase marks the access token rejected without deleting the vault record; the next operation attempts one refresh. Do not repeatedly retry 401s without diagnosing permissions or server errors. If the blob exceeds the Windows vault limit it fails without falling back to plaintext. A process interruption between server-side token rotation and the vault write can still require reauthorization. The refresh request uses Python's TLS trust and proxy environment; Git-only private CA or proxy settings may require compatible system/Python configuration. Keep TLS verification enabled. A simulated refresh does not prove the live server's eventual refresh behavior.

GCM's generic OAuth is another option when its actual version and store are compatible. GCM 2.7.0 fixes storage of rotated generic OAuth refresh tokens; assess whether that specific issue affects the requested flow before an authorized upgrade. Do not assume every GCM version refreshes proactively. Never use git-credential-oauth -verbose with real credentials: its debug output can contain tokens.

References: Git credential configuration, Gitea OAuth provider and scopes, git-credential-oauth, GCM credential stores, generic OAuth, GCM 2.7.0.