Access
When another device reaches this one, or an agent here asks the service for something, Alto checks the request against rules: who may do what, where. There are two kinds:
- Grants allow. Nothing from another device is allowed unless a grant allows it, and grants only add up, so adding one never takes anything away.
- Forbids take away. Any forbid that matches wins over every grant, wherever each came from.
There’s no order between rules: a request is allowed when some grant allows it and no forbid matches.
The built-in grants
Section titled “The built-in grants”These are always there, and are what Alto has always allowed:
| Who | May |
|---|---|
| You, on this device | Anything. Never refused, by any rule, so a broken file can’t lock you out of fixing it. |
| Agents on this device | What you may, unless a forbid says otherwise; see Agents. |
Your own devices (your Tailscale account, or a trust entry) |
Anything another device can ask, including what happened here and your other devices. |
| A device paired from another account | Read, type into, start, end and answer its sessions here, but not this device’s own information. |
Some things only this device can do, whatever the grants say: pairing, settings, trust, and the like.
Your own rules
Section titled “Your own rules”access.toml, beside settings.toml, holds your grants and forbids:
# Alex's devices may see what happens here, as your own do.[[grant]]src = ["alex@example.com"]dst = ["device"]verbs = ["read"]
# Agents here end only the sessions agents started.[[forbid]]src = ["agent:*"]dst = ["*"]verbs = ["kill"]unless = { dst = ["by:agent"] }because = "agents here end only what agents started"
# A group, for this file's rules.[groups]eng = ["alex@example.com", "*@eng.example.com"]src, who: a login (sam@example.com), a domain (*@example.com),device:NAME(a paired device, by the namealto devicesshows),tag:NAME(a tailnet tag),group:NAME(from this file’s[groups]),person,agent:NAMEoragent:*, or*.dst, where:*,sessions(any session, or a new one),own(the session the caller runs in, and those it started),device(this device’s own information: what happened here, your other devices), or selector words for sessions (by:agent,agent:claude,status:needs-you) and for this device (pool:NAME,tag:NAME,os:linux).verbs:read,send,spawn,kill,approve. Typing issend, but typing at an agent while it’s asking something is answering it, so it’sapprove(C-c or Escape alone staysend). A message to an agent (call … message) issendon its session; a method a program offers needs what itsalto offer --needssaid,sendby default.join: the modes a session is joined in,observer,peerandmoderator(see Joining). A rule needsverbsorjoin.- A forbid’s
unlessis where it doesn’t hold: from one of itssrc, at one of itsdst, or both (then both must match). Itsbecauseis told to whoever it refuses. - A group counts only in the file that defines it: your
group:engisn’t your organization’s.
A change takes effect at the next request, with no restart.
Agents
Section titled “Agents”An agent is checked twice: as you (or whoever it acts for), and as itself. It’s allowed only if both are, so it never gets more than its person; a grant to agent:claude only widens what the agent itself may.
What agents may do as themselves by default is [agents] scope in settings.toml:
[agents]# "open" (the default): whatever their person may.# "own": read anything; type into, start, end and answer only their own# sessions (the one they run in, and those they started).scope = "own"With "own", a grant gives an agent more:
[[grant]]src = ["agent:claude"]dst = ["pool:scratch"]verbs = ["send", "kill"]Watching, not typing
Section titled “Watching, not typing”A session holds everyone attached to what they may do there. A device that may read a session but not send sees it, and anything it types is refused, saying so, once:
alto: session 4: refused: not allowed to send session 4 on this device: watching only (its log says why: access-91be0c)Joining as an observer, a peer or a moderator
Section titled “Joining as an observer, a peer or a moderator”Whoever attaches to a session joins it in a mode:
| Mode | May |
|---|---|
observer |
Watch. |
peer |
Watch, type, and answer what the session asks. |
moderator |
All that, and pause agents there (alto pause) and end the session. |
alto attach --mode observer 4 joins as one; without --mode, a client joins as the most it may, so you and your own devices are moderators, as before. Commands that attach for a moment (send, pause, capture) take the most they may.
Rules decide the modes, through the verbs each needs (a peer send, a moderator also kill) and join:
# Alex's laptop watches, and nothing more.[[forbid]]src = ["device:alex-laptop"]dst = ["*"]join = ["peer", "moderator"]because = "watch only"
# The SRE group may type in prod, but not moderate there.[[grant]]src = ["group:sre"]dst = ["pool:prod"]join = ["observer", "peer"]- A forbid’s
joinrefuses those modes (it needs noverbs). Asking for one is refused at once, saying why; joining without--modetakes the most left. - A grant’s
joingives only those modes, each with what it may do, whatever the grant’sverbs. A grant withoutjoingives the modes its verbs reach. - Ending a session with
alto killis a moderator’s too: a forbid ofmoderatorstops it.
alto access check WHO MODE SESSION asks about a mode:
$ alto access check alex-laptop peer 4refused: joining as a peer needs read: access.toml, forbid 1 forbids alex-laptop read session 4: watch only; the most it may join as is an observerThe log says the mode each client joined in, and a mode refused.
Agents type only while someone watches
Section titled “Agents type only while someone watches”A forbid’s unless can ask for someone to be there:
[[forbid]]src = ["agent:*"]dst = ["pool:prod"]verbs = ["send"]unless = { present = { mode = "moderator", count = 1, from = "group:sre" } }because = "agents in prod type only while SRE watches"Unlike other forbids, it doesn’t refuse: an agent’s typing there waits until enough of the right people are attached, then goes through, in order. While it waits, alto ls says “agents wait for a moderator (agents in prod type only while SRE watches)” and the fleet view marks the session ⏸. A session started where the rule applies waits from the start.
mode:moderator(the default) orpeer; someone attached in that mode or more counts.count: how many (1 by default).from: who counts, assrcsays it (a word or a list); anyone, if it’s left out. You on this device count when it names you (person, say).- It holds typing only, so
verbs = ["send"]. A grant doesn’t get past it, and people typing aren’t held by a rule on agents.
alto access check agent:claude send 4 says when typing would wait, and for whom.
Who’s watching
Section titled “Who’s watching”Who’s attached, and how, is in the listings: alto ls says “watched by 2 people (1 observing), 1 agent”, alto describe names each, the fleet view counts them (◉ 3), and alto ls --in watched:no lists the sessions nobody is attached to.
$ alto describe 4session 4: agent, workingwatched by 2 people (1 observing), 1 agent you, moderator alex@example.com on alex-laptop, observer claude in session 2, moderatorWhen a file is broken
Section titled “When a file is broken”A grant that can’t be read is access not given, which is safe; a forbid that can’t be read would be access given. So:
access.tomlbroken (it can’t be read, or says something Alto doesn’t know): the last rules it had that read cleanly stay in force, forbids and all, until it reads again.alto settings,alto access checkand the service’s log say so. Taking the file away takes its rules with it.- Your organization’s
managed.tomlbroken: nothing from another device or agent is allowed until it reads again. You, on this device, still may.
Your organization’s rules
Section titled “Your organization’s rules”Managed settings take the same [[grant]], [[forbid]] and [groups], beside their settings. Their forbids hold as yours do, and you can’t change them.
Your tailnet’s grants
Section titled “Your tailnet’s grants”A tailnet’s policy can grant Alto access in its grants, as an app capability; Tailscale has already decided who:
{ "src": ["group:sre"], "dst": ["tag:alto"], "app": { "altoterm.com/cap/alto": [{ "dst": ["sessions"], "verbs": ["read"] }] }}Each value takes dst, and verbs or join, as above. They’re read from Tailscale with each connection, so they last only while the policy says them. The tailnet carries grants only: a forbid there would be lost whenever Tailscale couldn’t be asked, so one is ignored, and the service’s log says so. Forbids go in managed.toml.
Asking
Section titled “Asking”alto access check WHO VERB WHERE gives the decision and the rule behind it, without doing anything:
$ alto access check paired read devicerefused: no grant lets a paired device read this device's own information; nearest: built in: devices paired with this one$ alto access check alex@example.com read deviceallowed by access.toml, grant 1$ alto access check agent:claude kill 4refused: access.toml, forbid 1 forbids claude, for you, on this device kill session 4: agents here end only what agents startedWhen the answer lets another login type here, it adds a warning: whoever may type into a shell may run anything as you.
WHO is me, agent:NAME, yours (one of your own devices), paired (a device paired from another account), a paired device’s name, or a login. WHERE is a session’s id, new, or device. It exits 1 when refused; --json gives allowed, reason, file and problem.
Whoever is refused is told what they asked, where, and a forbid’s because, not the rules, with a reference to this device’s log, which has the whole reason:
alto: refused: not allowed to kill session 4 on this device: agents here end only what agents started (its log says why: access-3fa2c1)The reference is in the log too: alto log --json | grep access-3fa2c1.
What rules can’t hold
Section titled “What rules can’t hold”- Typing into a shell is everything. Whoever may
sendto your shell can run anything there as you, including editingaccess.toml. For other people, the line that holds isreadagainstsend. - Rules about agents are as strong as knowing an agent is one. Alto knows an agent by the program it runs under; one that leaves it (as a background service) is taken for you.
- Sessions started before this version don’t hold agents or other devices to the rules. Restart them (
alto reload --restart) to bring them in.