Skip to content

GitHub App credentials follow the launch checkout, not the repository being pushed #253

Description

@danny-avila

With --github-repository-routing checkout, each command gets one repository-scoped installation token, chosen from the origin of the checkout its launch cwd resolves to (gitHubRepositoryForCommand in packages/code/src/github.ts, resolved in cli.ts before launch). Git then presents that token for every github.com URL through http.https://github.com/.extraheader. Any command that reaches a different repository after launch presents the wrong token and gets:

remote: Permission to <owner>/<repo>.git denied to <app>[bot].
fatal: unable to access 'https://github.com/<owner>/<repo>.git/': The requested URL returned error: 403

Agents do this routinely: cd ../other-repo && git push, git -C <other checkout> push, a clone of another repository nested inside the admitted checkout, a worktree of repo A stored under repo B's .worktrees/, or pushing a remote other than origin. The 403 names the bot, so it reads like a permission or expiry problem, and agents learn to retry, which never helps.

Evidence from one trusted-VM worker over 9 days: 49 of 257 agent pushes were denied. None were 401s or expired tokens. Every denial with a known launch context matched the patterns above (nested clones under the LibreChat checkout, cross-repo worktrees), or predated routing (a fixed installation that did not cover a transferred repository).

Related transfer hazard: installation lookup uses redirect: 'error' and caches the installation per owner/name string for the life of the process. A repository transferred to another owner keeps working under its old remote URL for git, but the worker's lookup for the old name fails hard, or reuses a stale installation.

Proposal

  • Resolve credentials per request URL instead of per launch cwd. A git credential helper (or a per-URL extraheader) that mints a token for the repository in the URL being fetched or pushed, and returns nothing for repositories outside the admitted set, would cover cd, git -C, nested clones and non-origin remotes. Mint lazily and cache per repository as today.
  • For gh, keep the launch-checkout token as the default, but let GH_REPO/--repo resolve their own token the same way if feasible.
  • Follow a transfer: on a 301 from /repos/{owner}/{name}/installation, re-resolve with the canonical full_name (and cache under both names) instead of failing.
  • When a token for repository X is presented to Y, fail with an actionable message where the worker can detect it (for example, a credential helper request for an unadmitted repository), naming both repositories.

Acceptance

  • A command launched in checkout A that pushes checkout B (sibling, nested, or via git -C) succeeds when B is admitted and its installation covers it.
  • A push to an unadmitted repository fails with a message naming the repository, not a bot-permission 403.
  • A checkout whose origin still uses a transferred repository's old owner authenticates correctly.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions