Part 6 of 6 Running one for other people
An agent's own credential
By the end of this chapter, an agent has an account of its own on the team's instance, reaches the website rebuild and nothing else, and what it writes arrives under its own name rather than under the name of whoever set it up.
Install it, and tell your agent did this on Laurence's own laptop, where the agent runs on the machine that holds the work and one command finishes the job. Here the agent runs somewhere else. You issue the credential on the server and hand it over, and the difference shows up immediately, in the flag you leave off.
The work so far
Laurence has the instance of An account, an install and a first run, reached at the address TLS, a proxy and systemd gave it, and a project for the work:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin subroutine
sudo python3 -m venv /opt/subroutine
sudo /opt/subroutine/bin/pip install "subroutine[postgres]"
sudo install -d -o subroutine -g subroutine -m 0755 /var/lib/subroutine
sudo -u subroutine env \
XDG_CONFIG_HOME=/var/lib/subroutine/config \
XDG_DATA_HOME=/var/lib/subroutine/data \
XDG_STATE_HOME=/var/lib/subroutine/state \
/opt/subroutine/bin/subroutine init --workspace metacortex --instance-name MetaCortex
sudo -u subroutine tee -a /var/lib/subroutine/config/subroutine/config.toml > /dev/null <<'SETTING'
public_url = "https://tasks.example.com"
SETTING
sudo -u subroutine env \
XDG_CONFIG_HOME=/var/lib/subroutine/config \
XDG_DATA_HOME=/var/lib/subroutine/data \
XDG_STATE_HOME=/var/lib/subroutine/state \
/opt/subroutine/bin/subroutine project create web "Website rebuild"
sudo -u subroutine env \
XDG_CONFIG_HOME=/var/lib/subroutine/config \
XDG_DATA_HOME=/var/lib/subroutine/data \
XDG_STATE_HOME=/var/lib/subroutine/state \
/opt/subroutine/bin/subroutine serve
One command, and the flag to leave off
sudo -u subroutine env \
XDG_CONFIG_HOME=/var/lib/subroutine/config \
XDG_DATA_HOME=/var/lib/subroutine/data \
XDG_STATE_HOME=/var/lib/subroutine/state \
/opt/subroutine/bin/subroutine agent create claude --profile worker --project web | tee /tmp/for-the-agent
subroutine agent create makes the account, puts it in the workspace with a role, and issues its credential, in one act. That matters more than it looks: an account with no membership authenticates perfectly and can do nothing, which reads as a broken token rather than as a missing role, and over a network the alternative is three requests with a half-finished agent if the second fails.
--store is the flag to leave off here, and it is the one Install it, and tell your agent uses. It records the credential as this machine's agent, and this machine is the server. The agent does not run here. Recording it here would do nothing for the agent and would quietly narrow the commands you run on this server.
So the last two lines are true and expected rather than a problem: nothing on this server will ever present this credential.
The line beginning Checked is not an echo of what you typed. It is what the instance answered when the command presented the credential it had just minted. A scope naming a permission the role does not carry, or a pin on a workspace the account cannot reach, turns up on that line rather than on the agent's first call, a week later, as an error somebody else has to interpret.
What a profile says
--profile names a situation instead of assembling one out of flags, and it expands into exactly those flags: there is nothing a profile can express that you could not have typed.
| Profile | Reaches | Writes in | For |
|---|---|---|---|
worker |
one project, and everything filed under it | the same | an agent that owns a project |
collaborator |
the projects named | the ones named with --write |
an agent that reads related work for context and writes only its own |
observer |
the projects named, or the whole workspace | nothing | a reporting or reviewing agent |
colleague |
one workspace | the same | a second person, working in it as they would in their own |
A combination that means two things at once is turned down rather than resolved:
sudo -u subroutine env \
XDG_CONFIG_HOME=/var/lib/subroutine/config \
XDG_DATA_HOME=/var/lib/subroutine/data \
XDG_STATE_HOME=/var/lib/subroutine/state \
/opt/subroutine/bin/subroutine agent create nosy --profile observer --write web
A credential that quietly does something other than what you just described is one nobody checks again.
Verbs and items are separate questions
--scope decides which verbs a credential carries. --project decides which items it can reach at all. They are different axes and it is worth naming them separately, because most mistakes here are somebody reaching for one and meaning the other.
subroutine token create is the same work without an agent behind it, which is what a scheduled job wants:
sudo -u subroutine env \
XDG_CONFIG_HOME=/var/lib/subroutine/config \
XDG_DATA_HOME=/var/lib/subroutine/data \
XDG_STATE_HOME=/var/lib/subroutine/state \
/opt/subroutine/bin/subroutine token create --service-account reporter --scope task:read --title "weekly digest" | tee /tmp/for-the-reporter
And the other axis:
sudo -u subroutine env \
XDG_CONFIG_HOME=/var/lib/subroutine/config \
XDG_DATA_HOME=/var/lib/subroutine/data \
XDG_STATE_HOME=/var/lib/subroutine/state \
/opt/subroutine/bin/subroutine token create --service-account webbot --workspace metacortex --project web
It brings the sub-projects with it, which is why the command says so rather than repeating what you typed: a restriction that stopped at one level would be useless on any tree deeper than one. Everything outside it is not merely missing from a listing. The project does not resolve, so the credential is told there is no such project rather than that it may not look.
Name a project by its key, which is unique within a workspace rather than across the instance. Where two workspaces both hold a web, the command asks which rather than choosing, because an agent pointed at the wrong tree works perfectly against the wrong tree.
What the bound is, over the wire
The credentials are in the two files those commands were teed into. These lines take them out, exactly as a reader would paste them:
agent=$(grep -o 'sr_[A-Za-z0-9_-]*' /tmp/for-the-agent | head -1)
reader=$(grep -o 'sr_[A-Za-z0-9_-]*' /tmp/for-the-reporter | head -1)
The read-only credential reads:
curl -so /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $reader" https://tasks.example.com/v1/tasks
and does not write:
curl -s -H "Authorization: Bearer $reader" --json '{"title":"nope"}' https://tasks.example.com/v1/tasks | grep -o '"detail":"[^"]*"'
Read that refusal carefully, because it distinguishes two things most do not. The role would have allowed the write; the token is narrower. An agent that reads this knows it has been deliberately bounded rather than misconfigured, which is the difference between getting on with something else and filing a bug against you.
The agent's own credential does write, in its own project:
curl -so /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $agent" --json '{"title":"Fix the deploy script","project":"web"}' https://tasks.example.com/v1/tasks
It cannot widen itself
The obvious worry about handing a credential to something that writes its own commands is that it issues itself a better one. It cannot:
curl -s -H "Authorization: Bearer $agent" --json '{"service_account":"wider","scopes":["task:read","task:write"]}' https://tasks.example.com/v1/tokens | grep -o '"detail":"[^"]*"'
Issuing a credential affects the whole installation rather than one workspace, so it needs a permission the contributor role these accounts get does not carry. That holds however the credential is scoped, and it is worth knowing before you hand one out rather than after.
Whose name it arrives under
sudo -u subroutine env \
XDG_CONFIG_HOME=/var/lib/subroutine/config \
XDG_DATA_HOME=/var/lib/subroutine/data \
XDG_STATE_HOME=/var/lib/subroutine/state \
/opt/subroutine/bin/subroutine journal
An agent, and whose. That is the whole reason for giving it an account rather than lending it yours: the record says a machine did this, and says which account answers for it, and neither fact can be recovered afterwards from a change that carries somebody's name.
The account it answers to is whoever ran agent create, which on this server is the service account rather than a person. There is no flag for it, and nothing here can supply one: the command records the account it was run as. Where it matters that an agent answers to the person whose work it does, that person issues the credential themselves rather than asking you to issue it for them.
Handing it over
The credential goes to the client as SUBROUTINE_TOKEN. It is never accepted in a query string and never read from config.toml, so there is nowhere it can end up by accident.
The address goes with it, and where the instance holds more than one workspace it carries ?workspace=, as An agent, with nothing installed uses it. One workspace and there is nothing to say.
At the other end there are two places to put it, and which one depends on how much of that machine the agent should be:
--store, on their machine, which makes it that machine's agent everywhere. Install it, and tell your agent is that arrangement in full.- A project's own settings, where one repository gets an agent of its own and the rest of the machine carries on as before:
Not checked: the build does not run this.
{
"env": {
"SUBROUTINE_TOKEN_WORK": "sr_…"
}
}
That goes in .claude/settings.local.json in the project's directory. The name after SUBROUTINE_TOKEN_ is the connection's, upper-cased, with anything that is not a letter or a digit as an underscore, so a server added as work gives SUBROUTINE_TOKEN_WORK. Claude Code hands every entry in that block to everything it starts in that project, which is both of the ways an agent reaches an instance at once. Tell them to check the repository ignores that file before a credential goes into it.
The half you cannot do from here
A credential is resolved per process, not per agent. An agent reaches an instance two ways at once: through the tools its editor wired up, and by running subroutine in a shell. Those are separate processes and each finds its credential for itself, so configuring the tools does nothing about the shell, which finds whatever the command line finds, normally the person's own.
The result of getting one half right is an agent that is itself half the time and the person the other half. That is worse than plainly acting as the person, because it is partial: anybody spot-checking finds the agent's name on the half that went through its tools and concludes the setup worked.
Credentials are looked for in this order, and the first one found wins:
SUBROUTINE_TOKEN_<CONNECTION>in the environment, the connection's name upper-casedSUBROUTINE_TOKEN, for the default connection only- whatever the connection's own
token_envortoken_commandnames credentials.toml, which holds the agent's token where one is stored and the process is an agent's, and the person's otherwise
Only the last step can tell the two apart, and it does so by the variable the editor sets on everything it starts. So the thing to ask for, from whoever runs that machine, is the pair of checks rather than either one: subroutine whoami in the agent's shell, subroutine_whoami through its tools, and the same command in their own terminal. Install it, and tell your agent runs all three.
With that, the credential is issued, bounded and proved from this end, and what it writes arrives under its own name. Everything else about it is somebody else's machine, which is exactly the boundary this chapter is drawn along.