Skip to content

feat: run each project as its own git account - #1

Merged
getBoolean merged 31 commits into
mainfrom
feat/project-git-accounts
Nov 1, 2026
Merged

getBoolean merged 31 commits into
mainfrom
feat/project-git-accounts

Conversation

@getBoolean

@getBoolean getBoolean commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Problem

People with more than one forge account had no way to tell T3 Code which one a project belongs to. Agents committed with whatever name and email they chose, and pushes and PRs went out as whichever account was signed in on the machine, so switching meant signing out and back in (for example in GitHub Desktop) before each project.

Change

You can now add several git accounts and pick one per project, with a global default. Every agent session, terminal, setup script, and server-side commit, push, and PR in that project then runs as that account: its author name and email, its credentials, and its forge login. If a project's account can't be used, T3 Code warns first and only falls back to this machine's login for a thread you confirm.

  • Accounts. Each has a label, author name and email, forge and host, and either a token T3 stores (redacted to clients) or an existing gh login, plus an optional SSH key. Covers GitHub and Enterprise, GitLab, Forgejo/Gitea, Bitbucket, Azure DevOps, and GitCafe. Managed in Settings → Source control on web and desktop, and in mobile settings.
  • Per process, not global. The identity goes into each process's environment (GIT_CONFIG_COUNT entries, a host-scoped credential helper, url.insteadOf, core.sshCommand, and the forge's token variables), so two projects can run as different accounts at the same time without touching ~/.gitconfig. An opt-in per-project switch also writes user.name/user.email into the repo's own config, so terminals and GitHub Desktop match; turning it off restores the previous values.
  • When an account can't be used (deleted, token or gh login missing, SSH key missing, remote on another host), sending a message, committing, pushing, opening a PR or opening a terminal shows a warning that says why and names the machine git author and gh login the thread would use instead. Continuing turns on a bypass for that thread only, shown by a Machine git login badge that also turns it off. The bypass lives in server memory and ends when the account works again or its settings change. Without a bypass the server still fails closed, and a failed project-store read now counts as unusable instead of silently using the machine login.
  • Terminals follow the account live. T3 terminals put git and gh shims first on PATH. Each call sources a per-terminal env file (owner-only, removed on close) that the server rewrites when settings or a bypass change, so open terminals pick up switches without reopening. A shell profile that prepends its own git keeps the start-up account.
  • SSH aliases like git@github-work:org/repo (resolved with ssh -G) and forge SSH hosts like ssh.dev.azure.com count as the account's host.
  • Changing an account takes effect before the next turn. A live session finishes its turn and then restarts. A thread on a shared session (Codex, OpenCode) moves to its own session.
  • Providers that can't take the environment (Cursor, and OpenCode pointed at a server URL) are refused in projects that have an account.
  • Published mobile app. Shipped App Store and Play builds can't get new screens, so t3_project_git_account lets an agent list accounts, request a switch, or request a bypass. Both apply only after the user types the confirmation phrase in their own message from web or mobile, so an agent can't do either by itself. Newer mobile builds show the same warning natively. t3_project_read now names the project's account and whether it comes from the project or the default.
  • PR list viewers are resolved per host and account, so two projects on github.com with different accounts each see their own "my PRs".

Session-settled decisions carried from planning: identity plus credentials plus forge login (user-directed, over identity-only); machine login or stored token (user-directed); all supported forges (user-directed, over GitHub-first); fail closed (user-approved, over warn-and-fall-back); global default plus project override (user-directed); restart before the next turn (user-approved, over interrupting); per-process enforcement plus opt-in repo config (user-directed); switching from the published app through an agent with user confirmation (user-directed).

Scope and approval

This is a fork feature requested by the fork's maintainer. It builds on what already exists (project-scoped settings, provider adapters, and per-forge credential lookups). Stripping agent co-author trailers is a separate global option and is not part of this PR.

Verification

  • Focused tests for account resolution, the environment overlay (including a real git run checking that commits use the account's author and that a blocked account fails), session restart and moving off a shared session, the switch confirmation, repo-config write and restore, each forge's account credential, and terminal environments. Results: server suites 185/185 for the changed areas, forge packages 880/880, terminal 81/81.
  • Review fix: the GitHub stack rebase pushed with the account's token but committed as "T3 Code". There is now a test that goes through the stack action itself, and it fails without the fix.
  • The Linux and macOS CI lane (.github/workflows/git-account-tests.yml) passed on earlier commits. A run on the latest commit is in progress.
  • A real Claude (Haiku) turn in a bound project: the agent's shell got the account's identity and token, and an unusable account failed closed.
  • Browser pass on the web dev build: the Git accounts list, the Add account dialog, and the project Git account picker with the repo-config switch all render and work.
  • Bypass flow in the web dev build: with the account made unusable, sending showed the warning with this machine's real git author and gh login; Confirm sent the message and showed the badge; clicking the badge ended the bypass; Cancel kept the draft unsent.
  • check:changed on Windows: the only new failures were the agent context budget and settings search order, both fixed. The rest also fail on this Windows machine without these changes (SSH error classification, Claude attachments, worktree recovery, an Antigravity path check, an ACP shell test).
  • The MCP tool list agents see grows by 777 bytes (agentContextBudget.json updated); upstream CI would need the agent-context-increase label.
  • Not checked: a real Codex turn (codex CLI not installed here), the published mobile app on a device (no simulator), and the paid evals for the new MCP tool text (no API keys). Before/after screenshots of the settings were reviewed in the session but haven't been uploaded yet.

Review findings

  • P1 — t3_project_read now shows the account; t3_project_git_account handler tests and an evals/ case added (dry-run only, no model keys here); project-settings doc updated.
  • P1 — The model picker still doesn't explain why Cursor and OpenCode-with-server-URL are refused in a bound project (left as a follow-up). The settings browser test was added.
  • P2 — A project-store read failure now fails closed (and can be bypassed per thread).
  • P2 — SSH-alias and forge SSH host remotes are accepted.
  • P2 — The PR list resolves its viewer per host and account.

Review run 20261101-012503-261590d8; artifacts were kept locally at %TEMP%\compound-engineering-197609\ce-code-review\20261101-012503-261590d8.


Compound Engineering

Claude Opus 5.5 via Claude Code, running in T3 Code.

getBoolean and others added 17 commits October 31, 2026 21:56
Adds git accounts (label, author identity, one forge host, a forge
token or CLI login, optional SSH key) with a global default and
project-scoped overrides. Account tokens use the settings secret store
and are redacted for clients; editing accounts needs providers:manage.
GitAccounts resolves the project override, then the global default, and
builds the environment that makes git and forge CLIs act as the account:
author/committer, a host-scoped credential helper, SSH/HTTPS rewrites,
the forge token variable, and removal of inherited tokens. Unusable or
deleted accounts get a blocked overlay that fails commits, pushes and
forge calls naming the account instead of falling back to the machine.
When a project has a git account, PullRequestService runs its forge
calls with that account's token, which replaces the saved host token,
GH_TOKEN, the gh login and the GitHub App sign-in on every supported
forge. An unusable account fails closed naming it.
Stacked actions resolve the project's git account, check it against the
push remote's host before anything is committed, and run the commit,
push and PR step under it. The GitHub stack rebase accepts the account
so rebased commits and their push use it. Hidden checkpoint commits
keep the T3 Code identity.
… config

Terminals and project setup scripts start with the project's git
account; an unusable account opens the terminal with git blocked and a
notice naming the account. RepoConfigSync writes the account identity
into the repo's local config when the project opts in and restores the
previous values when it opts out, unbinds or the account is deleted.
Adds a git-account test lane on Linux and macOS runners.
GitVcsDriverCore.test.ts has macOS-only /private/var path failures that
predate this work, so the lane runs only its git-account case.
Moves EnvironmentOverlay and applyEnvironmentOverlay next to the
provider instance environment so provider adapters can apply a
project's git account to the processes they spawn.
ProviderSessionManager resolves the thread's git account when it opens a
session, hands the adapter the account overlay, and remembers the
account fingerprint. A session reused under a different account is
released and reopened; a shared session that serves other threads
reports ProviderSessionAccountChangedError instead. Adapters must
declare appliesAccountEnvironment, so one that can't run as the
account is refused in bound projects rather than using the machine's
identity.
…count

When a shared provider session (Codex, OpenCode v2) runs as a different
git account than the thread's project, turn start records the thread on
a session of its own and starts again, so the turn runs as the right
account without disturbing the other threads on the shared process.
Codex, Claude, Antigravity, Grok, Fx, the ACP registry, Pi, Muse and
OpenCode (when it starts its own server) apply the git account overlay
to the agent process and, for ACP agents, to client terminals, and
declare appliesAccountEnvironment. Cursor runs in-process and OpenCode
with an external or pooled server can't take one project's account, so
bound projects refuse them.
Settings > Source Control lists git accounts with add, edit and remove
(removal says which projects it blocks), editable with providers:manage.
The project rows pick the account and opt into writing it to the
repo's git config; at environment scope they set the default.
The mobile Source control page gets the Git account choice and the
write-to-repo-config switch, with project overrides, plus a Git
accounts section to add and remove accounts on a single environment.
The account helpers move to client-runtime so web and mobile share them.
t3_project_git_account lists accounts and switches the calling thread's
project to one. A switch first returns a phrase the user must type in
the thread; the server applies it only after a user-authored web or
mobile message with that phrase, so an agent can't rebind a project on
its own. This works from the published mobile app with no new screens.
Drop the unused GitAccounts.list method and summary type, reuse
launchIdentity's shellQuote for the failing credential helper, and cache
gh login tokens for 30 seconds so each turn, terminal and forge call no
longer spawns gh. Remove comments that restated the code.

Claude Opus 5.5 via Claude Code (T3 Code)
A GitHub stack rebase in a project bound to a git account pushed with the
account's token but committed as "T3 Code <noreply@t3.codes>", because the
only caller never passed the account to cascadeRebaseStack. The forge
account credential now carries the account's author name and email, and
the stack action passes it through.

Claude Opus 5.5 via Claude Code (T3 Code)
@github-actions github-actions Bot added vouch:trusted Author is trusted by repo permissions or the VOUCHED list. size:XXL labels Nov 1, 2026
Test User added 11 commits November 1, 2026 01:09
Claude Opus 5.5 via Claude Code (T3 Code)
A project whose account can't be used blocked git everywhere, with no way
through. A thread can now bypass the problem: it then runs as this
machine's git and gh login, which the status names, until the account
works again or its settings change. Other threads keep failing closed.

A failed project-store read now resolves to a blocked identity instead of
silently falling back to the machine login, and a remote on another host
is part of the same check so it can be bypassed too.

Claude Opus 5.5 via Claude Code (T3 Code)
Clients can now ask a thread's git account status, including the machine
login it would fall back to, and turn a bypass on or off. Setting a
bypass needs the source control write grant, like pushing does.

The server now builds one GitAccounts instance shared by provider
sessions, terminals, git actions and the RPC handlers, so a bypass set
from a client reaches all of them.

Claude Opus 5.5 via Claude Code (T3 Code)
…count

A terminal kept the git account it started with, so a switch or a bypass only reached terminals opened afterwards. T3 terminals now put git and gh shims first on PATH. Each call sources the terminal's env file, which the server rewrites whenever settings or a thread's bypass change, then runs the real tool. Env files hold tokens, so they are owner-only, removed when the terminal closes, and cleared on start.

A shell profile that puts another git ahead of the shims still starts on the right account but won't follow later switches.

Claude Opus 5.5 via Claude Code (T3 Code)
Sending a message, committing, pushing or opening a pull request in a
thread whose git account can't be used now asks first. The warning says
why the account can't be used and which machine login the thread will
use instead. Continuing turns on that thread's bypass. Opening a
terminal shows the same warning without waiting, since open terminals
pick the bypass up live.

A badge next to the thread title shows the bypass and turns it off. The
status also covers draft threads, which the server doesn't know yet, by
passing their project.

Claude Opus 5.5 via Claude Code (T3 Code)
…confirms

The published mobile app can't show the new warning, so t3_project_git_account gains bypass and end_bypass. A bypass first returns the reason and the machine login the thread would use, plus a phrase; it applies only after the user types that phrase in their own message, the same rule as switching. Ending a bypass needs no confirmation, since it only restores the account. list now also reports whether the calling thread's account works.

Adds an mcp-tool-use eval case; it was dry-run only, since no model keys are available here.

Claude Opus 5.5 via Claude Code (T3 Code)
Mobile builds that include this now ask before sending, running a git
action or opening a terminal in a thread whose git account can't be used,
with the same wording as web. The git panel shows when a thread uses the
machine login and taps back to the account. Older published builds keep
the agent route from the previous change.

Claude Opus 5.5 via Claude Code (T3 Code)
…count

A remote like git@github-work:org/repo, the usual way to keep two accounts apart in ~/.ssh/config, or Azure's ssh.dev.azure.com was refused because its host didn't match the account's. The check now resolves an alias with ssh -G and maps known forge SSH hosts to their web host before comparing, and still refuses a remote it can't place.

Claude Opus 5.5 via Claude Code (T3 Code)
Two projects on one host with different git accounts shared one viewer login, so filters like my pull requests and review requested could show the other account's work. Viewers are now resolved and cached per host and account; the list result still reports one viewer per host.

Claude Opus 5.5 via Claude Code (T3 Code)
An agent could only learn which account a project commits as by calling the git account tool. t3_project_read now includes the effective account's id, label and whether it comes from the project or the default, never its credential. A deleted account keeps its id so the agent can say which one is missing.

Claude Opus 5.5 via Claude Code (T3 Code)
Editing an account must leave its saved token alone, since the client only ever holds the redaction marker, and removing one must say which projects lose it first.

Claude Opus 5.5 via Claude Code (T3 Code)
Test User added 3 commits November 1, 2026 05:15
Claude Opus 5.5 via Claude Code (T3 Code)
The git account MCP tool and the project read's account field add 777 bytes to the tool list agents see, so the committed budget moves to the measured size. The git account settings now sort after the fetch interval in search, so a search for git still leads with it.

Claude Opus 5.5 via Claude Code (T3 Code)
@getBoolean getBoolean added the agent-context-increase Accepts growth in the agent context budget label Nov 1, 2026
@getBoolean
getBoolean merged commit 4c1c117 into main Nov 1, 2026
20 of 23 checks passed
@getBoolean
getBoolean deleted the feat/project-git-accounts branch November 2, 2026 03:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-context-increase Accepts growth in the agent context budget size:XXL vouch:trusted Author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant