Approval gate

The rules, command
by command.

Every agentic SSH tool says it asks before doing anything dangerous. Almost none of them will tell you where the line is. This is ours, written out: the exact allowlist that runs unattended, the characters that stop a command dead, and what the card puts in front of you before anything reaches a shell.

Enforced in the app's core before a command reaches the shell, not by the model · applies only to commands the agent proposes, never to your own terminal · the read-only allowlist and the metacharacter rule ship in 0.1.6 today; risk classification, the denylist and the per-command allowlist land in the next release

Default-deny, in one sentence

A command the agent wants to run executes without asking only if it matches a fixed read-only allowlist and contains no shell metacharacters. Everything else stops.

That ordering matters. It is not "block the dangerous things" — a blocklist is only ever as good as your imagination. It is "run the handful of things that cannot change the server, and stop for everything else, including everything nobody thought of." The failure mode is asking too often. We will take it.

Runs On the read-only allowlist, no shell metacharacter anywhere in the line, no state-changing verb. It runs on the agent's own SSH channel and the output appears in the chat.
Waits Everything else. A card appears with the verbatim command and nothing happens until you click. Unrecognised commands land here too — not knowing is a reason to ask.
Auto-approve on A per-host toggle, off by default. While it is on, that host skips the card and every command the agent proposes runs. Nothing is held back underneath it in this build — read the section below before you use it.

The read-only allowlist

The complete list, not a representative sample. Some entries qualify with any arguments; some only in an exact bare form, because env reads your environment and env FOO=bar ./thing executes a program.

Command Qualifies
ls cat head tail less wc grep stat file find With any arguments
du df free ps id echo which uname lsb_release With any arguments
netstat ss journalctl With any arguments
pwd whoami hostname date uptime env printenv top htop Bare only — no arguments
ip Only ip a, ip addr, ip route
git Only status, log, diff, show, branch, remote
systemctl Only status, is-active, list-units, show
docker Only ps, images, inspect, logs, stats, version, info
kubectl Only get, describe, logs, top, version

That is the whole list. No sed, no curl, no ssh, no editor, no package manager, no systemctl restart. If a command you use constantly is missing, that is deliberate rather than an oversight — the list only grows for commands that cannot write.

A second check runs before it. Even a command that starts with an allowlisted word is sent to a card if the line also contains a state-changing verb — sudo, rm, mv, dd, chmod, chown, kill, apt, npm, pip, crontab, iptables, tee, a systemctl or service start/stop/restart, a mutating docker, kubectl or git subcommand, and the rest of that family.

What a metacharacter actually is

The word gets used on every page like this one and explained on none of them. A metacharacter is a character the shell reads as punctuation rather than as text — an instruction about how to run the line, not part of the thing being run. They are how one harmless-looking command smuggles in a second one.

Character What the shell does with it
; Ends this command and starts another one on the same line.
&& || Runs a second command depending on whether the first succeeded or failed.
& Detaches the command to run in the background, unwatched.
| Feeds this command's output into a different command — which can be any command at all.
> >> < Redirects: writes the output into a file (overwriting it), or reads a file into the command.
`…` $(…) Command substitution: runs the inner text first and pastes its output into the line.
$ Expands a variable, so what finally runs is not what you read.
( ) { } Groups commands into a subshell or a block — the machinery a fork bomb is built from.

Any of those anywhere in the line and the command does not auto-run, full stop. ls -la /etc is a read. ls -la /etc > /tmp/out is a write. ps aux | grep nginx is two commands, and the gate is not going to reason about the second one.

The test is deliberately dumb: a character scan over the whole string, not a shell parser. It does not care that the ; in grep 'a;b' file.log is quoted and harmless — it sees the character and sends the command to a card. That is a false positive, and we would rather have it than a parser that gets clever about quoting and is wrong once.

Worked examples

The same two rules, on an ordinary day.

Ten lines the agent might propose, and where each one lands. No third rule is hiding behind these.

Command Result Why
ls -la /var/www Yes Runs Allowlisted, no metacharacters, nothing state-changing.
journalctl -u nginx -n 200 Yes Runs Allowlisted with any arguments — reading logs is the job.
git log --oneline -20 Yes Runs log is one of the six read-only git subcommands.
grep 'a;b' /var/log/syslog No Waits The ; is quoted and harmless. The gate does not parse quotes — it sees the character and stops.
ps aux | grep nginx No Waits A pipe. Both halves are harmless here; the gate does not check the second half.
cat /etc/hosts > /tmp/hosts.bak No Waits A redirect writes a file. That is not a read.
systemctl restart nginx No Waits restart is not one of the four read-only systemctl subcommands.
npm install No Waits Package managers change the host, and this one runs arbitrary install scripts.
./deploy.sh No Waits Unrecognised. Default-deny means unknown is a stop, not a shrug.
rm -rf /etc No Waits rm is a state-changing verb, so it stops. The gate does not rank it above npm install — that judgement is yours, on the card.

What the card shows you

The card is the security boundary. It is deliberately small and deliberately literal.

  1. A status line

    A coloured dot, NEEDS APPROVAL, the host the command would run on, and a WRITE or DESTRUCTIVE tag when the command text earns one. The host name is there because the wrong-window mistake is the expensive one. The tag is a reading aid inferred from the text, not a second gate — everything on a card stopped for the same reason.

  2. The exact command

    Verbatim, monospaced, selectable, with a copy button. Not a summary, not a paraphrase of what the agent intends — the literal string that will be handed to the shell. What you read is what runs.

  3. Approve or Deny

    Two buttons, no default, no timer bar creeping toward "yes". Nothing executes until you press one. Under them sits Approve & don't ask again on this host, which runs the command and turns on auto-approve for that whole host — broader than the wording suggests, so read it as the setting it is. You can switch it back off from the host's controls.

  4. Then it collapses to a receipt

    Once you decide, the card shrinks to one line: state, command, exit code, duration. Click it to expand the output. All of it — command, decision, exit code, output — is stored with the conversation in the SQLite file under ~/.tryssh, on your machine; commands that stopped for approval are additionally written to an audit_log table in the same file.

Nobody home is a "no". If you close the app, drop the connection, or simply move on and send the agent another message, any card still waiting settles as skipped and the agent is told so. An unanswered gate is never resolved as approval.

Auto-approve, and what it costs

The default for a new host is ask, and the setting is per host — there is no global switch that quietly covers every server you have added.

It is one toggle, not a graded autonomy level: only read-only commands run automatically, or every command on this host runs automatically. The card's don't-ask-again button flips it. Be plain about what that means in this build — there is no denylist held back underneath it, so a host with auto-approve on will run an rm -rf / the agent proposes, unattended, with no card. The toggle exists because "check disk usage across nine boxes" is unbearable one card at a time. Turn it on for a staging box. Think hard before anything that takes payments.

One thing sits outside the gate entirely. A watcher — a command the agent registers to re-run on a timer — is created without an approval card in this build. The command is printed in the chat when the agent creates it and stands in the Watchers tab, where you can disable or delete it, but nothing stops at creation time. If you let the agent set up watchers, read what it registered.

The gate reads text, not intent. It is a pattern matcher over a command string. It is not a sandbox, not a simulator, and it does not know what your ./deploy.sh does. It is good at the shapes of catastrophe people actually type, and it will never be complete — which is exactly why the default is to stop for anything it does not recognise, and why the card puts the literal command in front of you instead of a summary you would skim.

Limits

What the gate does not do.

Four things it is not, written out, so none of them has to be inferred from what we left unsaid.

>_

It does not govern your terminal

You type whatever you want. The gate sits only in front of commands the agent proposes, on the agent's own SSH exec channel — a separate channel from your PTY, which the agent can neither read nor type into.

Your shell

It does not overrule you

It will not stop you approving something destructive. That is the point of a human in the loop. If you click through DESTRUCTIVE on web-prod at 2am, the command runs.

Human in the loop

It does not sandbox anything

An approved command runs with whatever privileges your SSH user has. If that is root, it is root. It makes no claim about what happens after a command has run, and none about a host that is already compromised.

No sandbox

It is not a certification

We hold none and imply none. This page is an architectural description you can hold us to, not an attestation — the security page writes that distinction out rather than leaving it to inference.

No claims

Read the command. Then decide.

The gate's honest description is narrow: it makes the agent unable to change your servers by accident, and unable to change them quietly. Everything past that is you reading the card.