-
Notifications
You must be signed in to change notification settings - Fork 4
CLI Machines Remote
ed machines exec runs one command on a configured machine over the SSH
connection Edith already holds, hands you the machine's stdout and stderr on
your own stdout and stderr, and exits with the status the remote command
exited with. ed <machine> <command...> is the same thing with the ceremony
removed: name a machine as the first word and the rest of the line runs there.
This is the escape hatch under every other ed machines verb. docker,
files, power and services exist because a parsed, --json answer is
worth having for the things you script; everything else on the machine is
reachable by typing it. Nothing here needs the Edith app to be running, because
the transport is /usr/bin/ssh and a ControlMaster socket on disk rather than a
request to the app.
| Command | What it does |
|---|---|
ed machines exec <machine> <command...> |
Runs the command over the shared connection with no pty, streaming both output channels and propagating the remote exit code. |
ed machines exec --tty <machine> <command...> |
The same, on a real terminal, which is what vim, top, a sudo password prompt and docker exec -it need. |
ed machines run <machine> <command...> |
Alias of ed machines exec. |
ed <machine> <command...> |
Shorthand that rewrites to ed machines exec <machine> -- <command...>. |
ed <machine> |
A bare machine name with nothing runnable after it is ed machines show <machine>. |
ed <machine> cd [<directory>] |
Sets the working directory the later commands on that machine run in, per terminal. |
ed machines <machine> <command...> |
The machine-first spelling: a word after machines that is not a subcommand is moved to wherever that subcommand wants it. |
ed __complete |
Hidden. Emits completion candidates, and hands over to the machine for anything after a machine name. |
Runs one command on one machine and gets out of the way. Both output streams
stay separate, the remote exit code becomes yours, and the only byte ed adds
is the newline it puts back on a last line that had none.
ed machines exec [--tty] <machine> [--] <command...>
ed machines run [--tty] <machine> [--] <command...>
| Name | Type / values | Default | What it does |
|---|---|---|---|
<machine> |
machine name, ssh config alias, id, or an unambiguous prefix of a name or alias | none, required | Which machine to run on. Matched case-insensitively: exact name first, then ssh alias, then UUID, then a unique prefix. |
<command...> |
one or more words | none, required | The command to run. A single leading -- is dropped before anything is sent. |
| Name | Type / values | Default | What it does |
|---|---|---|---|
--tty, -t
|
flag | off | Allocate a terminal for the command with ssh -tt. Needed by anything interactive; wrong for scripting. |
--help, -h
|
flag | off | Print the help for this command on stdout and exit 0. |
--version |
flag | off | Print the CLI version, 1.0.0, and exit 0. Generated by the parser and accepted here as well as at the root. |
Every one of these has to come before the machine name. The command array is
captured for passthrough, so the parser stops reading options at the first
positional word: in ed machines exec tuf --tty top the --tty is part of the
command and is sent to the machine, where it is not a program:
$ ed machines exec tuf --tty top
bash: line 1: --tty: command not found
$ echo $?
127
There is no --json. ed machines exec is one of the handful of commands that
do not offer it, because the bytes on stdout belong to the remote process and
wrapping them would break every pipe. --json before the machine name is an
unknown option and exits 2.
ed machines exec tuf uptime
ed machines exec tuf -- ls -la /etc
ed machines exec --tty tuf top
ed machines run tuf systemctl status nginx
Nothing local is mutated except the remembered working directory, which only
cd writes, and what opening the connection leaves behind: the ControlMaster
socket and the host key pinned beside it. The machine is resolved from Edith's
own machine list, so a host is never retyped, and the connection is opened
before the command runs: if a ControlMaster socket for that machine is already
alive, from the app or from an earlier ed, the command is one round trip on
it, and if there is not, ed opens one that outlives the process by
ControlPersist=10m.
How the words become a command line depends on how many there are. One word is sent verbatim, which is the one-shot escape hatch for anything the remote shell has to interpret. Several words are quoted individually and joined, so an argument with spaces survives and a shell metacharacter does not:
$ ed tuf echo 'a;pwd'
a;pwd
$ ed tuf 'ls /etc | head -3'
adduser.conf
alsa
alternatives
A word is left unquoted only when every character in it is a letter, a digit or
one of . _ - + / = : @ % ,. Everything else is wrapped in single quotes, which
covers spaces, ~, *, $, |, >, ;, & and quotes themselves. So
ed tuf ls '*.log' does not glob on the machine, and ed tuf ls '~/Desktop'
does not expand the tilde; quote the whole line instead when you want either.
--tty builds its command line differently: the words are joined with single
spaces and nothing is quoted, because the point of a terminal session is to let
the remote shell parse the line. The same input therefore behaves differently
under the two paths:
$ ed machines exec --tty tuf echo 'a;pwd'
a
/home/pulkit
Shared connection to 192.168.1.12 closed.
That trailing line is ssh's own, on stderr, and appears on every --tty run.
Stdin is not forwarded on the plain path. The remote process is given
/dev/null, so printf 'x\n' | ed tuf cat prints nothing and
ed tuf wc -l < file counts zero. With --tty your terminal is handed
straight to ssh and typing works, but a redirected file or a pipe still will
not end the command: the remote side sees a terminal rather than a closed pipe,
so a cat with nothing more to read waits instead of exiting. Feed data to a
machine with ed machines files put or by quoting a line that reads the file on
the far side.
Output is read line by line off two pipes and re-emitted, stdout to stdout and
stderr to stderr, so a partial last line with no newline is still printed and
the exact interleaving of the two streams is not guaranteed to match what the
machine produced. There is no timeout: ed tuf tail -f /var/log/syslog streams
until you interrupt it.
The exit code is the remote command's, verbatim and unclamped, which is the one
documented exception to ed's 0 to 4 contract:
$ ed tuf exit 42
$ echo $?
42
Naming no command at all fails before anything is dialled, and the error names the machine you gave:
$ ed machines exec tuf
error: name a command to run, for example `ed tuf uptime`
The command runs in whatever directory ed <machine> cd last set for this
terminal, on both the plain and the --tty path. That is a cd prefix on the
line ssh is given, cd '<directory>' 2>/dev/null || cd;, so a directory that
has since been deleted quietly falls back to the home directory rather than
failing every command after it.
Names a machine as the first word of the line and runs the rest there. It is
pure argument rewriting: ed reshapes argv before the parser ever sees it,
then the ordinary ed machines exec runs.
ed <machine> <command...>
ed <machine>
The rewrite happens in this order, and stops at the first rule that applies:
| When the first word | The line becomes |
|---|---|
is missing, or starts with -
|
unchanged |
is machines
|
reordered by the machine-first rule below |
is a top level command name or alias, or help, or __complete
|
unchanged |
| does not match a configured machine name or ssh alias | unchanged |
matches a machine, and something after it does not start with -
|
machines exec <machine> -- <rest> |
| matches a machine, and the rest is empty or all flags | machines show <machine> <rest> |
The match against your machines is by whole name or whole ssh alias,
case-insensitively. It is deliberately stricter than the resolver ed machines exec then uses: a prefix or a UUID is not enough to trigger the shorthand, so
ed tu uptime is not rewritten and fails as an unknown command with exit 2,
while ed tuf uptime and ed 'asus tuf 7' uptime both run.
The reserved list that wins over a machine name is every top level command and
alias: guide, schema, version, completions, install, uninstall,
config, app, extensions, permissions, usage, system, music,
nowplaying, np, calendar, tools, apps, download, downloads, dl,
clipboard, color, colour, shelf, cleaner and machines, plus help
and __complete. A machine called usage is unreachable by the shorthand and
has to be named explicitly, as ed machines exec usage -- ... or
ed machines show usage.
Because the rewrite inserts -- after the machine name, every flag after it
belongs to the machine, never to ed. That is what lets ed tuf ls -la and
ed tuf docker compose up -d work without ceremony, and it is why --tty has
no shorthand: write ed machines exec --tty tuf top.
ed tuf docker ps runs the machine's own docker binary and prints its raw
output. The parsed, --json version is ed machines docker ps tuf, which is a
different command. Both are correct; the shorthand is always the raw one.
A word straight after machines that is not one of the group's subcommands is
treated as the machine and moved to wherever the subcommand wants it. ed walks
as far down the subcommand tree as the words allow, then inserts the machine
after the last subcommand it consumed:
ed machines tuf uptime ed machines exec tuf -- uptime
ed machines tuf docker ps ed machines docker ps tuf
ed machines tuf files ls /etc ed machines files ls tuf /etc
ed machines tuf run ls -la ed machines run tuf -- ls -la
ed machines tuf ed machines show tuf
Unlike the top level shorthand this does not check your machine list at all, so a typo is still moved into the machine slot and the error names it rather than complaining about an unknown subcommand:
$ ed machines nosuchbox uptime
error: no machine named nosuchbox
hint: known machines: Asus TUF 7; machines subcommands: add, broadcast, connect, disconnect, docker, edit, exec, files, forward, forwards, kill, list, ls, metrics, power, remove, rm, run, services, show, snippet, snippets, workspace, workspaces
A subcommand name always wins. The names that cannot be used as a machine here
are ls, list, show, add, edit, rm, remove, forwards, forward,
snippets, snippet, power, workspace, workspaces, broadcast, kill,
metrics, exec, run, files, docker, services, connect and
disconnect. A machine literally called docker needs
ed machines show docker.
ed tuf uptime
ed tuf systemctl status nginx
ed tuf 'ls -la /srv | head'
ed machines tuf docker compose ls
A bare machine name is a lookup, not a shell:
$ ed tuf
Asus TUF 7
target tuf · pulkit@192.168.1.12
auth SSH agent
system Linux 7.0.0-28-generic x86_64
uptime 22:25:13 up 9:17, 5 users, load average: 0.03, 0.08, 0.21
session pulkit on seat0 since 2026-08-08 13:08 (login screen)
session pulkit on tty2 since 2026-08-08 13:08 (tty2)
Flags alone count as nothing runnable, so ed tuf --json is
ed machines show tuf --json rather than an attempt to run --json on the
machine.
Sets the directory the later commands on that machine run in. It is not sent to
the machine as a command; ed intercepts it, asks the machine where that path
resolves to, and records the answer.
ed <machine> cd <directory>
ed <machine> cd -
ed <machine> cd
| Name | Type / values | Default | What it does |
|---|---|---|---|
<directory> |
a path on the machine, absolute or relative to the current one, or -
|
omitted | Where to move to. Omitted means the home directory; - means back to the directory you were in before the last cd. |
cd is only intercepted when it is the whole command and there are at most two
words. ed tuf cd and ed tuf cd Desktop are interceptions;
ed tuf cd a b and the quoted one-shot ed tuf 'cd /tmp && pwd' are ordinary
commands that run and change nothing:
$ ed tuf 'cd /tmp && pwd'
/tmp
$ ed tuf pwd
/home/pulkit
ed tuf pwd /home/pulkit
ed tuf cd Desktop
ed tuf pwd /home/pulkit/Desktop
ed tuf ls lists Desktop
ed tuf cd - back to where you were before
ed tuf cd back to the home directory
A successful cd prints nothing and exits 0. Under the covers ed runs
pwd; cd -- '<target>' && pwd on the machine, prefixed with a cd into the
directory you were already in, and keeps two lines: the first pwd becomes the
previous directory and the second becomes the current one. That is why -
works, and why it toggles rather than walking a stack.
The pair is stored in a file at
~/Library/Application Support/Edith/machines/cwd/<machine>/<session>, where
<machine> is the first ten characters of the machine's id with the dashes
removed and <session> is the name of the terminal your stdin is attached to,
/dev/ stripped and anything that is not a letter or a digit turned into a
dash. So /dev/ttys012 becomes ttys012, and two tabs on one machine never
move each other, the same way cd behaves in a local shell. When stdin is not a
terminal, which covers a pipe, a script and a cron job, the session is called
shared and every such invocation uses the same slot. The directory is created
mode 0700 and the file is written atomically.
A path that does not exist is reported with the machine's own message, exits 1, and leaves the stored directory alone:
$ ed tuf cd nosuchdir
error: cannot change to nosuchdir on Asus TUF 7
hint: bash: line 1: cd: nosuchdir: No such file or directory
cd - with nothing recorded for this terminal exits 1 with no previous directory for <machine> in this terminal, decided locally once the connection
is open and without asking the machine anything. The round trip a cd does
make is under the 60 second command timeout that ed puts on ordinary
commands, unlike the command path, which has none.
--tty reads the remembered directory but never sets it. The terminal branch
runs before the cd interception, so ed machines exec --tty tuf cd Desktop
changes directory inside that one pty session and nothing survives it.
The remembered directory reaches exactly two things: ed machines exec,
including the shorthand and the --tty form, and remote path completion.
Everything else keeps its own defaults, so ed machines files ls tuf still
lists the remote home directory after ed tuf cd Desktop, and the app's own
terminal and Finder windows are unaffected.
The hidden command behind shell completion. The installed zsh, bash and fish scripts call it with the whole word list and the index of the word being completed, and it prints one candidate per line. You never type it, but what it does after a machine name is the interesting half of this page.
ed __complete --index <n> -- <words...>
| Name | Type / values | Default | What it does |
|---|---|---|---|
--index |
integer, zero based | 0 |
Which word in <words...> is being completed. Word 0 is the program name. |
<words...> |
the command line so far | empty | Captured for passthrough, so flags in it are data. A single leading -- is dropped. |
When the first word after the program name is not a known command and does
match a configured machine, ed stops consulting its own tree and asks the
machine. What it asks depends on where the cursor is:
- At the first word after the machine name it asks for command names, with
compgen -c -- <prefix> | sort -u | head -2000. That completes against the remotePATH, including toolsedhas never heard of. - After
cd,pushdorrmdirit asks for directories only, withcompgen -d, capped the same way. - Anywhere else it uploads a small bash harness that sources
/usr/share/bash-completion/bash_completionor/etc/bash_completion, runs_completion_loaderfor the command being typed, finds that command's registered-Ffunction withcomplete -p, calls it withCOMP_WORDS,COMP_CWORD,COMP_LINEandCOMP_POINTset the way bash would, and printsCOMPREPLY. When the command has no completion function or produces nothing, it falls back tocompgen -o default, which is filenames.
So ed tuf docker <TAB> runs docker's own completion on the machine rather
than a list baked into ed:
$ ed __complete --index 3 -- ed tuf docker comp
compose
Two guards keep this from ever being slow. It runs only when a ControlMaster
socket for that machine is already alive, checked with ssh -O check, so
pressing TAB never dials a sleeping host; with no open connection you get no
candidates and exit 0. And the round trip itself is capped at six seconds, after
which the candidate list is empty rather than late.
The whole probe is prefixed with the same cd that commands get, so completion
follows ed <machine> cd. Candidates are filtered by the prefix you have typed,
case-sensitively, and deduplicated in the order the machine returned them.
The half-typed word is never interpolated into the remote line unquoted. The
command-name probe shell-quotes it, and the directory probe passes it to
bash -c as a positional parameter, so a prefix such as $(touch /tmp/pwned)
is completed against rather than run.
Under ed machines <machine> ... completion behaves the other way round: the
words are reordered the way the parser will see them and ed's own tree
answers, so ed machines tuf <TAB> offers docker, files and the rest of the
group's verbs rather than remote programs.
| Code | When |
|---|---|
| 0 | The remote command exited 0, or a cd landed. --help also exits 0, and so does a completion probe with nothing to offer. |
| 1 | No command word was given; a cd the machine refused; cd - with no previous directory recorded for this terminal; ssh could not be started at all. Also produced when the remote command itself exits 1. |
| 2 | The command line was wrong: an unknown option before the machine name, such as --json, or a missing machine argument. Also produced when the remote command itself exits 2. |
| 3 | The machine did not resolve: nothing is configured, the name is unknown, or a prefix matched more than one machine. The hint lists the candidates and the machines subcommands. |
| 4 | The connection could not be opened. The message is could not reach <machine>: <reason>, with the reason translated from ssh's own text: authentication failed, connection refused, timed out, could not resolve the host name, or the host key changed. |
| anything | The remote command's own status, passed through unchanged, so 127 for a command the machine does not have, 130 for one you interrupted, and anything else a program chooses to return. |
Codes 1, 2, 3 and 4 are also values a remote program can return, and ed cannot
tell you which side produced one. When a script needs to know, look at stderr:
ed's own failures always start with error: and never touch stdout.
- The exit code passthrough is the single documented hole in the CLI's 0 to 4
contract.
ed machines exec, the shorthand,ed machines docker logsanded machines docker compose logsare the only commands that do it. -
--ttyis the counterpart of the app's Machine terminal pane, and of the Docker window's shell button, which ised machines exec --tty <machine> 'docker exec -it <container> sh'. - The plain path gives the remote process no terminal at all. Anything that
checks
isattywill disable colour and progress bars, which is usually what you want from a script and never what you want fromtop. - A single leading
--is stripped, once.ed machines exec tuf -- -- lssends-- ls. - The shorthand and the machine-first spelling both leave a word alone when it
starts with
-, soed --helpanded machines --helpare never mistaken for machine names. -
ed <machine>with only flags after it ised machines show, which opens a connection and runsuname,uptimeandwhoon the machine. It is not a free lookup. - The remembered working directory is per terminal, not per shell. Two panes in
the same terminal emulator have different
ttysnames and so different directories; a subshell inside one pane shares its parent's. - Everything a pipe touches uses the
sharedsession slot, soed tuf cd /tmptyped at a prompt does not change where a cron job'sed tuf makeruns, and two concurrent scripts do share one slot. - The ControlMaster socket lives at
~/Library/Application Support/Edith/machines/sockets/<hash>.sk, keyed by the same ten characters of the machine id that name the working directory folder.ed machines disconnect <machine>closes it, which also silences remote completion until something opens it again. - Host keys are pinned in Edith's own
known_hostsbeside that socket, with your~/.ssh/known_hostsconsulted as well andStrictHostKeyCheckingset toaccept-new. Do not shell out tosshdirectly for a configured machine; you lose the shared connection and the pinning. - Neither the Edith app nor the menu bar helper has to be running for anything on this page, and no macOS permission is involved.
-
ed machines exectakes no--json, and neither does the shorthand. If you want structured output, run something on the machine that produces it and pipe the result intojqyourself. -
ed machines broadcastis the many-machine version of the same idea: one line on every configured machine, output labelled per machine, and exit 1 if any of them failed rather than the remote status. It does not honour the remembered working directory.
-
ed machinesfor the machine list itself, connecting and disconnecting, and the saved forwards and snippets the terminal pane uses. -
ed machines dockerfor the parsed,--jsonhalf of whated <machine> docker ...reaches raw. -
ed machines filesfor moving files, which is also how you get data onto a machine given that stdin does not travel. -
ed machines powerfor reboot, wake, systemd units and killing a process by pid. -
Getting started for installing the completion scripts
that call
ed __complete. - Conventions and contracts for the exit code table this page is the exception to.
-
The
edcommand line for the rest of the reference.
Auto-generated from docs/, edit the docs in the repo, not the wiki.
CLI reference
Companion
- Deploy
- Concepts
- Concepts Memory
- Concepts Ingestion
- Concepts Search
- Concepts Chat
- Concepts Learning
- Concepts Brain
- Concepts Friend
- Hosts
- Stack
- Status
- Doctor
- Search
- Index
- Ingest
- Episodes
- Sync
- Observations
- Reflect
- Beliefs
- Ask
- Extract
- Claims
- Corroborate
- Runs
- Chat
- Conversations
- Forget
- Export
- Import
- Erase
- Wipe
- Episode
- Nightly
- Reason
- Personas
- Council
- Lenses
- Core
- Why
- Hypotheses
- Predictions
- Commitments
- Discrepancies
- Calibration
- Inquire
- Entities
- Eval
- Standup
- Machines
- Baselines
- Connectors
- Facts
- Correct
- Weekly
- Db
Guides