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.
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.
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.
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.
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. |
The card is the security boundary. It is deliberately small and deliberately literal.
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.
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.
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.
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.
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.
Four things it is not, written out, so none of them has to be inferred from what we left unsaid.
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 shellIt 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.
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 sandboxWe 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 claimsThe 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.