"An agent did it" stops being an answer the moment you run more than one.
On a single developer machine you can now have Claude Code, Codex, OpenCode, Copilot, Antigravity, or pi all working in the same repo, sometimes in the same hour. The default setup — one git config user.name, one GitHub login, one .env — works fine right up until something goes wrong and you have to answer a question that has no answer: which agent wrote this, and what was it allowed to touch?
This post compiles five write-ups from September–October 2026 where teams actually did this, including the mistakes. Everything below is attributed at the end; nothing here was reproduced by me, so treat the numbers as claims with sources rather than verified facts.
The cost of one shared identity
Vitruvian Software runs ten agents in one monorepo — Forge on Bazel, Atlas on infrastructure, Ridge on Kubernetes, Wren on application code, Scout on tests and CI, Aegis on security review, Quill on docs. Until recently, GitHub saw all of them as one person: every push, PR, and merge went out under the repository owner's account.
That looked harmless and cost them three things:
| What you think you have | What you actually get |
|---|---|
| Attribution | "Ridge opened #1440" is a claim in a chat log. Neither git nor GitHub can back it up. |
| Peer review | GitHub blocks self-approval, and every PR looks self-authored — so a security agent cannot give a valid approval on an infra agent's diff. |
| Audit | The trail says one human did everything, including work that human never saw. |
The second one is the one that hurts. If you want agents to check each other, the platform has to believe they are different identities. With one identity, the best you can do is have an agent leave a comment and hope someone reads it.
Layer 1 — Git: a token decides who can push, an environment variable decides who signed
This is the failure that silently corrupts history, and it is easy to miss.
# 1) who pushes (API + git push)
export GH_TOKEN="<installation token>"
# 2) who is named in the commit — completely independent of the token
export GIT_AUTHOR_NAME="vitruvian-wren-agent[bot]"
export GIT_AUTHOR_EMAIL="<bot_user_id>+vitruvian-wren-agent[bot]@users.noreply.github.com"
export GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"
export GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"
Vitruvian set only the token first. Result: the PR showed the bot, and the commit log still showed the machine owner — because git takes authorship from local config and environment, not from whoever pushes.
Three details worth copying:
- Use the bot's noreply address. GitHub ties it to the App, so commits link back to
xxx[bot]. - One-hour installation tokens are a feature. The only long-lived secret is the private key, which lives in a vault; the token is minted per session and never pasted anywhere.
- Set both
GIT_AUTHOR_*andGIT_COMMITTER_*. Change only the author and a rebase or cherry-pick leaks the real operator into the committer field.
Layer 2 — Platform: GitHub Apps, not one bot account per agent
The obvious fix is a machine user per agent. GitHub's terms allow one free machine account per person, so ten agents would mean ten accounts the terms don't cover plus ten sets of credentials to rotate. GitHub Apps have no such limit: an org can own as many as it likes, each gets a real name[bot] identity that genuinely authors commits and submits reviews, and each authenticates with short-lived installation tokens instead of passwords or PATs. They named them vitruvian-<agent>-agent.
The question the docs don't answer
Before building, they needed to know whether an App's approval counts toward a require 1 approving review rule. The docs on protected branches say approvals come from "people with write permissions" or a designated code owner — Apps aren't mentioned either way.
So they measured it on a canary repo with required_approving_review_count: 1 and enforce_admins: true:
before reviewDecision=REVIEW_REQUIRED mergeable_state=blocked
POST /pulls/1/reviews event=APPROVE (App installation token)
after reviewDecision=APPROVED mergeable_state=unstable
review <app-slug>[bot] type=Bot state=APPROVED
It counts. (unstable was an unrelated pending check.) One limit surfaced: an App cannot be a code owner — CODEOWNERS accepts users, teams, and email addresses only. So they turned off "require code owner review" and used the approval count as the lever.
The step you cannot automate
App creation has no API. You either fill in a web form or use the App manifest flow, which is better because it returns the private key in an API response instead of a browser download:
{
"name": "vitruvian-<agent>-agent",
"public": false,
"default_permissions": {
"contents": "write",
"pull_requests": "write",
"metadata": "read",
"checks": "read",
"workflows": "write"
},
"default_events": []
}
GitHub redirects back with a one-time code you exchange within the hour for the App id and private key. That is the only copy of the key: straight into the vault, response deleted.
Two traps: the install screen preselects "All repositories" (accept it and every agent can write to every repo in the org — choose "Only select repositories"), and workflows: write is only for agents that should touch CI.
Make "who can write where" reviewable
repo_config:agentApps:
- name: beacon
installationId: "152031070"
# ...one entry per agent
repo_config:requiredApprovals: "1"
Pinning each App's installation to the repo turns authorization into a diff you review, not a checkbox on a settings page nobody opens.
And ordering matters. requiredApprovals: "1" must be last. Enable required reviews before the agents have identities and there is nobody who can approve: every PR waits at REVIEW_REQUIRED forever, and release-please / Dependabot auto-merges get stuck too, because a ruleset bypass covers direct merges but never auto-merge into a merge queue.
Safe order: create and install one App per agent → put keys in the vault and point each harness at its key → add every agent to agentApps → then raise requiredApprovals.
Day to day, an agent takes on its identity with one command:
eval "$(bazel run //tools/agent-app -- env wren)"
It looks the agent up in a committed registry (App ids, installation ids, bot user ids are all public identifiers), signs a JWT, exchanges it for an installation token, and exports both the token and the git identity.
Layer 3 — Inside the commit: who writes the three lines
A git author field holds a name and an email. It cannot hold "which job, which tool, which model". Obi (a Rust agent execution engine) appends three trailers instead:
Oby-Job: 3f2a9c1e
Oby-Agent: Codex
Oby-Model: gpt-6.1-sol
Now a teammate reading git log can tell which code an agent wrote and with what, without asking the owner — and the owner can query a model's track record:
git log --grep "Oby-Model: gpt-6.1-sol"
Why the engine must write them, not the agent
This is the most transferable idea in the whole design:
- trailers are written during
keep_commits, when a finished job's commits are packed back into the engine's clone; - each new commit is rewritten directly — read the raw object with
git cat-file commit, repoint parents, append the trailers, write the new object withgit hash-object -t commit -w --stdin; -
deliberately not
git commit-treeorgit interpret-trailers, which readcommit.gpgSign/trailer.*and can change the bytes; - tree, author, committer, and both dates stay exactly as they were;
- if the agent wrote
Oby-*lines itself, they are removed first — only the engine gets to say this; - writing the same trailers twice produces the same commit id (idempotent).
Two more choices: Oby-Agent records the tool (Claude Code, Codex, Cursor), not an account; and nothing ever records an account name, email, or hostname, because commits can be public.
Compare with "make the agent add a Co-authored-by line": the agent can forget, can get it wrong, and can be talked out of it by an injected instruction. Attribution written by a layer that does not reason is attribution you can trust.
Layer 4 — Granularity: one action, one commit
Orquesta takes the other route: don't rewrite trailers, make every action a commit. Their argument is that today's tools either dump changes into the working tree or produce one mega-commit at the end ("AI refactor") — and that isn't traceability, it's a black box with a receipt.
The mechanism:
- the agent works on a real branch cut from base;
- every file edit, code-changing shell command, and test-fix cycle is committed, with a Conventional-Commits-style message;
- the author field is the agent, via a separate git config:
git config user.name "Orquesta Agent"
git config user.email "agent@orquesta.local"
- read-only commands produce no commit (nothing changed), but they are logged in the activity feed.
The payoff is surgical rollback: when an agent both adds an endpoint and fixes an unrelated bug in one session, and the endpoint regresses, you revert one commit and keep the fix. Noise is handled by asking the agent to combine logically identical changes, plus a squash step before merge — granularity during execution, clean history after.
Layer 5 — Process and credentials: give a key to exactly one agent
Identity is not just a name; it is what the process can read.
Bill Mongan runs nine agents side by side on a small always-on Ubuntu box (~14 GiB RAM): Claude Code, Codex, Antigravity, Grok Build, Hermes, GitHub Copilot, two separate copies of OpenCode, and pi. Herdr (an agent-aware terminal multiplexer) gives each one a labelled tab and its own directory under ~/agents/<name>.
That directory separation matters more than expected: most CLIs resolve "resume the latest conversation" relative to the current directory, and several ask you to trust a workspace before touching it. One directory per agent keeps histories, trust decisions, and scratch files apart.
Most agents share a single mcpproxy instance for tools. One tool talks to a service that needs a personal API key, and only one agent should hold it. The trick:
- the provider declares the secret both as a server-side env var and as a caller-supplied header;
- the proxy's
.envgets the base URL only, and the key is left unset — so the proxy has no credential to hand out; - the key lives in one scoped OpenCode's private env file and is forwarded as a header:
"mcp": { "servers": { "mcpproxy": {
"type": "remote",
"url": "http://127.0.0.1:8888/mcp",
"oauth": false,
"headers": { "X-MCPProxy-Service-Key": "{env:SERVICE_API_KEY}" }
}}}
- the launcher is a small wrapper:
set -a; source service.env; set +a; exec opencode.set -aexports every assignment,execavoids leaving a shell process holding a copy — so the variable exists in that process tree and nowhere else.
Other agents can still see the tool; their calls simply fail to authenticate. When they need the capability, they delegate the task to the scoped agent instead of holding the key.
And the author is explicit that this is not a hard boundary
- any agent with a shell, running as the same Unix user, can read that
service.env; - the header travels over plain HTTP, acceptable only because the proxy listens on loopback;
- real isolation between agents would mean separate Unix users or containers.
So: least privilege by process environment, not a security boundary — and not treated as one. Its value is that other agents can't use the key by accident, and can't leak it through a tool that dumps environment variables.
What identity does not stop
Be clear about this, or you'll think you're done. Vitruvian published two incidents from their own setup:
| Incident | What happened | Fix |
|---|---|---|
| Merge queue bypass |
main let repo admins skip the merge queue as a human emergency path. Agent sessions under that account inherited the bypass: 5 of 40 merges skipped the queue in two days; one of them had been rejected by the queue eight minutes earlier, and main went red. |
A pre-tool hook refuses gh pr merge --admin, direct merges via REST/GraphQL, and pushes to main — and tells the agent to fix what the queue rejected. A human who really needs the bypass runs it in their own terminal, where the hook doesn't apply. |
| The worktree that uninstalled Argo CD | The homelab Pulumi config is gitignored, so it doesn't exist in a worktree → every toggle read false → one pulumi up removed Argo CD, its namespace, and every Application it managed. |
The program now refuses to run unless the config states the setting explicitly; and "never apply from a worktree" lives in AGENTS.md next to the incident date. |
Their conclusion is worth quoting in full:
Per-agent identity is a convention we keep, written into the
AGENTS.mdthat every coding tool in the repo reads: use your own name; minting as another agent puts their name on your work. It makes honest work attributable and reviewable. It doesn't make dishonest work impossible.
What actually stops corner-cutting is the other layer: guardrails written after real incidents, where every fix ships with the check that would catch it again.
Safeguard's October write-up names the five risks of "running as you": excess authority, blind audit trails, prompt injection, credential reach (tokens in env vars and dotfiles are readable by anything you run), and long sessions that accumulate context and access. Their rollout advice is practical: start with the highest-risk accounts and tasks (production, releases, cloud admin), keep everyday coding in a sandbox but remove long-lived secrets — and remember that if a scoped token takes a day and a ticket, developers will keep using their own identity. Tooling matters as much as policy.
A weekend checklist (dependency order, not priority order)
-
Decide who appears in history. Give each agent its own
GIT_AUTHOR_*/GIT_COMMITTER_*and a resolvable email. Ten minutes, immediate payoff:git logbecomes readable. - Make the platform agree. One GitHub App per agent (manifest flow), key in a vault, installation tokens only, "Only select repositories".
- Put attribution inside the commit. Either an engine that rewrites trailers (Obi) or one-commit-per-action with a distinct author (Orquesta). Do not rely on the agent to remember.
-
Narrow what the process can read. Per-agent config dirs and env files (mode 0600), header-injected or brokered credentials, nothing sensitive in a shared
.env. - Raise required reviews last. After step 2, so a different identity can approve.
-
Write guardrails, not hopes. Incident date in
AGENTS.md, every fix paired with a test that fails if it regresses, and "identity is attribution, not enforcement" as a stated design assumption.
If it's just you and three agents
The sources above are all team-scale. A lighter ladder:
| Where you are | Smallest useful move | Why it's enough |
|---|---|---|
Three agents on one user.name
|
per-agent GIT_AUTHOR_* / GIT_COMMITTER_* at launch |
One hour to a readable git log, zero platform changes |
| You want them to review each other | trailers + branch prefixes (agent/<name>/<topic>) |
Without platform identity, "peer review" is still human review; first make authorship legible |
| You want branch protection to actually bite | then GitHub Apps (or at least separate accounts) | "The platform accepts these are two identities" is the one thing env vars can't buy |
| You hold sensitive credentials | one config dir + one env file (0600) per agent | Keeps a key inside one process tree; the rest can't misuse it |
A wrapper is enough to start:
# ~/bin/agent-run — usage: agent-run wren claude
agent-run() {
local name="$1"; shift
export GIT_AUTHOR_NAME="$name"
export GIT_AUTHOR_EMAIL="$name@agents.local"
export GIT_COMMITTER_NAME="$name"
export GIT_COMMITTER_EMAIL="$name@agents.local"
export PI_CODING_AGENT_DIR="$HOME/.pi/agents/$name" # swap for your tool's config-dir variable
exec "$@"
}
And one check that fails loudly when you got it wrong:
git log -1 --format='%an <%ae> | %cn <%ce>' # expect the agent name, not yours
Boundaries
- This is a compilation, not a reproduction. The App-approval measurement, the trailer rewriting, the git-native flow, and the nine-agent box all come from the authors' own environments.
- Platform behaviour changes; "does an App approval count" and "can an App be a code owner" are both in the "not in the docs, measure it" category. Re-verify on a canary repo before relying on any number here.
- Naming (
vitruvian-*,Oby-*) is incidental. What transfers is the structure: who writes attribution, where it is stored, and which layer holds the credential. - Adjacent but different: sandboxing, egress limits, approval gates. Those answer "what can it do". This post answers "who does it count as".
Sources
- Vitruvian Software, Every AI agent gets its own GitHub identity, 2026-10-09 — https://vitruviansoftware.dev/blog/2026/10/09/every-ai-agent-gets-its-own-github-identity/
- Obi (
obi-ai-bot/oby) issue #1068, obid: commits say which job, agent and model wrote them, 2026-10-22 — https://github.com/obi-ai-bot/oby/issues/1068 - Orquesta, Git-Native AI Development: Every Action is a Commit, 2026-10-23 — https://getorquesta.com/blog/git-native-ai-development-every-action-is-a-commit-2026-10-23
- William M. Mongan, Herding Agents: Multi-Agent Coordination with Herdr and Per-Agent MCP Credentials, 2026-10-04 — https://www.billmongan.com/posts/2026/10/herdr-multiagent-mcpproxy/
- Safeguard Security Team, What are the identity risks when an AI coding agent runs as you, 2026-10-18 — https://safeguard.sh/resources/blog/when-your-ai-coding-agent-runs-as-you-identity-risks-and-fixes
- Background: Sumant Thakur, I Ran Multiple Coding Agents. The Code Was the Easy Part., 2026-09-30 — https://sumantthakur.substack.com/p/multi-agent-coding-workflow
Chinese original (full version, with the platform-specific details): 多 Agent 同机:Git 作者、凭据与提交归属怎么分
More on this site (in Chinese): AI 编程 / Vibe Coding 完全指南 · Agent / MCP / 自动化完全指南
Top comments (2)
The self-approval point is the sharpest one — if every agent commits as one identity, a security agent's review is structurally worthless, not just informationally weak. The same failure extends below git: one shared .env means a compromised agent session hands over every key in the machine, not just its own scope. Have the write-ups you compiled pushed per-agent identity down to the secrets layer too, or does attribution still stop at git and GitHub?
The detail about setting the token first and the commit log still showing the machine owner is exactly the kind of mistake that only shows up later during an audit. Good that you called out GIT_AUTHOR and GIT_COMMITTER separately.
Two things I'd add on the security side:
The self-approval point is the strongest argument for doing all this. Agents reviewing each other only means something if the platform sees them as different actors.