# Per-agent git identity **Status: idea, not implemented. Nothing in this document has been built.** Written 2026-08-10 from a read of the live spawn path; the file:line references were verified against `dc6b623`. ## The goal A team of agents works on this project, sometimes several of them in the same repository at once. Each one should commit under its own identity, so `git log` answers "which agent wrote this" without anybody having to remember to say so. Today it cannot. Every agent commits as the owner, because every agent *is* the owner as far as the OS is concerned. ## How git identity can be overridden at all Identity is unverified metadata, not authentication — a default, never a constraint. Anyone who can commit can claim any name and email, by any of: ```bash git -c user.name=X -c user.email=x@y.z commit # per-invocation config git commit --author="X " # author only; committer stays whoever ran it GIT_AUTHOR_NAME=X GIT_AUTHOR_EMAIL=x@y.z \ GIT_COMMITTER_NAME=X GIT_COMMITTER_EMAIL=x@y.z git commit # env; both identities ``` Precedence: `--author` > `GIT_AUTHOR_*` env > `-c user.email` > local config > global config. Two consequences that shape the design below: - **Author and committer are different fields.** `--author` alone leaves the committer as the owner, and `git log` shows only the author by default. Set both, or the attribution is half-fiction. To read both: `git log --format='%an <%ae> | %cn <%ce>'`. - **Environment beats instruction.** Telling an agent "commit as X" in its prompt is a rule it can forget. `GIT_AUTHOR_*` in the process environment applies to every git invocation in that process whether or not anyone remembered. The whole point is to make the identity unforgettable rather than well-intentioned, so this belongs in the environment. ## Why the obvious approach does not work here The first instinct is a PM2 `env` block per agent, or setting `process.env.GIT_AUTHOR_NAME` in the sidecar at boot. **Both are wrong on this platform**, and for the same reason: `officer-agent` is **one process running many concurrent sessions.** `sessions` is a `Map` (`src/servers/sidecar/claude/claude-manager.ts:213`); each session owns its own long-lived `query()` and its own warm `claude` child, reused across turns and collected after 30 minutes idle. A singleton lockfile enforces one sidecar per owner (`user-instance.ts:95-98`). So a process-level variable — whether from PM2, from `.env`, or assigned in `user-instance.ts` — is shared by every agent and every session on the box. It can say "an agent did this". It cannot say which. The `ecosystem.config.cjs` entry is bare anyway, which is worth recording since it looks like a place where env might already be happening: ```js { name: 'officer-agent', script: 'bun', args: 'run src/servers/sidecar/claude/user-instance.ts', watch: false }, ``` No `env`, no `cwd`, no `interpreter` — for any app in the file. ## What the spawn path actually looks like The live path is the Agent SDK, not `Bun.spawn`. `claude-manager.ts:320-360`: ```ts const q = query({ prompt: input.gen as AsyncIterable, options: { cwd: params.cwd ?? HOST_HOME, permissionMode: 'bypassPermissions', pathToClaudeCodeExecutable: CLAUDE_BIN, env: cleanEnv as Record, ... }, }); ``` `cleanEnv` is built once, at `claude-manager.ts:315`, by destructuring three keys back out of the sidecar's own environment: ```ts const { CLAUDECODE: _c, CLAUDE_CODE_ENTRYPOINT: _e, CLAUDE_CODE_SSE_PORT: _s, ...cleanEnv } = process.env; ``` That is the whole story: the child gets the sidecar's full `process.env` minus the three nested-session guards, and nothing is added per turn. **This is the good news.** `env` is *already* a per-`query()` option. It is built once today, but there is no structural reason it has to be — which makes `claude-manager.ts:315` the single injection point for everything below. ### What identity exists today Almost none, and none of it at the OS level. - `sessionKey` — officer's uuid, the key in the `sessions` map. Reaches the child only as a transport field on the pushed message. - **Agent name and persona are prompt-only.** `buildAgentPrompt` (`src/servers/api/agents/agent-runner.ts:71-79`) inlines the agent's `AGENT.md` into the *first user message*. There is no `systemPrompt`, no `--agents`, no per-agent settings file. - The one durable per-agent handle is the working directory: `getAgentRunsDir(agent.dirName)` (`agent-runner.ts:144`), deliberately shared across all runs of that agent so the CLI groups their transcripts. `grep -rn "GIT_AUTHOR\|GIT_COMMITTER" src/` returns nothing. Verified. Any commit an agent makes today is attributed to whatever `~/.gitconfig` says — the human owner, identically for every agent. ## The proposed change Three edits, all on the claude path: 1. **`src/servers/sidecar/protocol.ts`** — add an optional `gitIdentity: { name: string; email: string }` to `ClaudeSpawnStreamingParams` (the type begins at line 170). Optional so every existing caller is untouched. 2. **`claude-manager.ts:315`** — build `cleanEnv` per session rather than once, merging the identity in as all four variables when present: ```ts ...(params.gitIdentity && { GIT_AUTHOR_NAME: params.gitIdentity.name, GIT_AUTHOR_EMAIL: params.gitIdentity.email, GIT_COMMITTER_NAME: params.gitIdentity.name, GIT_COMMITTER_EMAIL: params.gitIdentity.email, }), ``` 3. **`agent-runner.ts:144`** — populate it from `agent.dirName`, next to where the cwd pin is already derived from the same field. Chat sessions (`api/chat/websocket.ts`) would pass nothing and keep committing as the owner, which is almost certainly right: a chat turn is the human driving directly. Use a domain that is actually controlled — `@officer.dev` — so Gitea can be made to map or deliberately not-map these authors later. ## Known limitations of the proposal ### opencode cannot do this at all Since the `serve` migration (`a3dbda7`, phase D) there is **no process spawned per turn**. One shared `opencode serve` starts at sidecar boot (`src/servers/sidecar/opencode/index.ts:112-116`) with no `env` key — full inheritance — and turns are driven over HTTP against it, with per-turn cwd carried as a request header (`serve-runner.ts:155`). Every session shares that one process environment. Per-agent git identity on the opencode path therefore requires either a serve per agent, or an upstream API field. Neither is a small change, and this document does not propose one. ### Attribution is not isolation, and isolation is the real problem There is **no filesystem isolation** between agents. They share one real `HOME` (`HOME_DIR=/home/pastilhas`), one `~/.claude`, one credential store; `user-instance.ts:75-78` says this outright, and it is the stated reason `chat` is an `execution` capability that can never be granted. `grep -ril worktree src/` returns nothing — worktrees are used nowhere. cwd is the only per-session variation and it is not a boundary, since absolute paths escape it freely. So two agents told to work on the same repository will share one working tree: fighting over `index.lock`, staging each other's half-finished edits, interleaving commits. Per-agent identity makes that **legible after the fact**. It does nothing to prevent it. If agents are genuinely to work the same repo concurrently, the isolation question is the larger and more urgent one — a worktree or a clone per agent — and per-agent identity composes naturally with it (a worktree per agent is also the cleanest place to put a per-worktree git identity). ### If worktrees do arrive, note this trap `git worktree` shares one `.git/config`, so `git config --local user.email` in one worktree changes it for **all** of them. Per-worktree config needs `extensions.worktreeConfig true` and then `git config --worktree user.email …`. Environment variables sidestep the whole issue, which is another argument for the env approach above. ## Deliberately not proposed - **Signing.** Per-agent SSH signing keys would make attribution unforgeable rather than conventional. For a trusted local fleet where every agent already runs as the owner with `permissionMode: 'bypassPermissions'`, an agent that wanted to forge another's identity has far easier routes. Revisit only if agents stop being equally trusted. - **`Co-Authored-By:` trailers.** Useful when a human and an agent genuinely share a commit, and both Gitea and GitHub attribute them. Orthogonal to this, and a prompt-level convention rather than an environment one. ## Open questions - Should pipeline steps (`pipeline-executor.ts`) get an identity too, or only named agents? - Is `dirName` the right identity, or should an agent's `AGENT.md` declare its own name and email — so the identity is authored where the persona is, rather than derived from a directory? - Does anything downstream — Gitea webhooks, activity feeds, the dashboards — assume commits belong to the owner and break when they do not?