AI agents on instances
ghostd is a small daemon for AI agents to work on a machine. It runs
commands as argument arrays and answers their exit codes and output, keeps
long-running work as jobs that outlive the connection, and reads, writes,
edits and searches files. It is reached over the SSH access you already
have, one kept connection, with nothing to quote. ghost is its client:
ghost x runs a command on an instance from a shell, and ghost mcp gives
an agent such as Claude Code its tools over MCP.
ghostd is open source, at
github.com/gigahost-apps/ghostd.
It runs as your user on the instance, with no service manager, and never
reads or changes anything under ~/.ssh.
Targets
Section titled “Targets”Every command and tool names the machine it works on, its target, in one of three ways:
| Target | |
|---|---|
| An instance ID or name | An instance of your account: ghost reads its public IPv4 address (and, for a shared IP instance, the public port that forwards to its SSH port 22) and logs in as root (--user changes that). This needs your account key, like ghost instance list. ghost looks a name up in your account every time, so that it always means the instance that has it now, and remembers an instance’s address for an hour; when the address stops answering, it looks the instance up again. |
ssh://[user@]host[:port] |
Any machine you can reach with ssh; your ~/.ssh/config applies, so a host alias works. The host is letters, digits, ., _ and - (or an IPv6 address in brackets), and so is the user. |
local |
This machine, without SSH: ghostd from PATH, or the binary GHOSTD_BIN names. An instance named local is named by its ID. |
ghost uses your own ssh, keys, agent and known_hosts. It runs SSH with
BatchMode=yes, so a key that needs a passphrase must be in your agent, and
it accepts a new host key the first time (StrictHostKeyChecking=accept-new).
After you reinstall an instance, its host key changes and SSH refuses it
until you remove the old one with ssh-keygen -R.
An instance with an address of its own answers SSH on port 22. A shared IP
instance needs a public port that forwards to its port 22; ghost instance ports INSTANCE lists and sets its ports.
Install ghostd
Section titled “Install ghostd”ghost ghostd install web-1ghost runs ghostd’s install script on the instance over SSH, which builds
ghostd from source: it needs a C compiler there (build-essential on Debian
and Ubuntu) and installs Rust for the user when it is missing, so the first
install takes a few minutes. It builds the latest release tag;
--ref REF names a tag, branch or commit. It installs ~/.local/bin/ghostd
(/usr/local/bin/ghostd as root) and ends by checking that ghostd answers:
ghost ghostd status web-1When you update ghostd and its daemon is running, the daemon keeps the
binary it started from, and ghost says so: run ghostd stop on the
instance, and the next connection starts the new one. Jobs keep running
through the stop.
Run commands: ghost x
Section titled “Run commands: ghost x”ghost x replaces ssh host 'command' for scripts and agents without MCP.
The arguments after -- are the program and its arguments, passed as they
are without a shell, so nothing needs quoting:
ghost x web-1 -- ls -la /etcghost x web-1 --cwd /srv/app -- git log -1 --format='%H %s'ghost x web-1 --env RUST_LOG=debug --timeout 600 -- cargo testghost x web-1 --stdin -- psql app < query.sqlghost x web-1 --script ./deploy.sh -- --fastThe program’s stdout and stderr are written to ghost’s own, byte for byte
and all of it, and ghost exits with the program’s exit code: 128 + n when a
signal n ended it, 124 at its timeout (120 s unless --timeout), 127 when it
could not be started, and 255 when ghost itself failed (the error is on
stderr). --login runs it through your login shell, for a PATH set in
~/.profile. With --json, ghost prints the result as JSON instead: the
exit code or signal, stdout and stderr (stdout_base64 when they are not
UTF-8), and the job’s ID.
Consecutive ghost x commands to one target share one SSH connection
(SSH’s own ControlMaster, kept 10 minutes, its socket in a private
directory under $XDG_RUNTIME_DIR/ghost).
Jobs and background processes
Section titled “Jobs and background processes”ghostd leaks no processes. A plain background process that a command leaves
behind (cmd &, nohup cmd &) is stopped when the command ends. Start a
server or anything long as a job instead; what it starts in the background
belongs to the job, which lives until none of it is left or you stop it:
ghost x web-1 --detach --name api -- ./server --port 8080ghost ghostd job list web-1ghost ghostd job logs web-1 JOB_ID --followghost ghostd job stop web-1 JOB_IDA job outlives the connection, ghostd’s restarts and its crashes; ghost ghostd job wait, get and remove complete the set. A process that
detaches into a session of its own (a daemon’s own forking, setsid) and a
service started with systemctl start are left running.
Copy files
Section titled “Copy files”ghost ghostd put web-1 ./release.tar.gz /srv/app/release.tar.gzghost ghostd get web-1 /var/log/app.log ./app.logBoth check the file’s SHA-256 at the end, and a file is replaced atomically: a reader never sees half of it.
Give an agent the tools: ghost mcp
Section titled “Give an agent the tools: ghost mcp”ghost mcp is an MCP server over stdio. Add it to Claude Code:
claude mcp add ghost -- ghost mcpor to Claude Desktop, in claude_desktop_config.json, with the full path to
ghost (Desktop starts it without your shell’s PATH):
{ "mcpServers": { "ghost": { "command": "/home/you/.local/bin/ghost", "args": ["mcp"] } }}Every tool takes a target as above, and answers a structured result; its
text is a one-line summary followed by the same result as JSON, for clients
that show the model only the text. A failure is a tool error that says what
ghostd or SSH said.
| Tool | |
|---|---|
ghostd_exec |
Run a program (an argument array, or a script) to its end: exit code, the last 64 KiB of stdout and stderr, and the job’s ID. output_file gets all of stdout. |
ghostd_job_start |
Start a program as a job and answer its ID at once. |
ghostd_job_list, ghostd_job_status, ghostd_job_wait |
The jobs, one job’s state, waiting up to 60 s for one to finish. |
ghostd_job_logs |
A job’s stdout or stderr: from an offset or its tail, or all of it into a local file. |
ghostd_job_signal, ghostd_job_stop, ghostd_job_remove |
Signal a job, stop it and all it started, remove a finished one. |
ghostd_file_read, ghostd_file_write |
Read a file as text (at most 256 KiB a call) or whole into a local file; write one from text or from a local file of any size. |
ghostd_file_edit |
Replace exact text in a file. |
ghostd_file_list, ghostd_file_search, ghostd_file_stat |
List a directory, search files with a regular expression as ripgrep does, a path’s metadata. |
ghostd_file_remove, ghostd_file_rename, ghostd_file_mkdir |
Remove, rename and make paths. |
ghostd_sync |
Make a directory on the target hold what a local directory holds, or the other way round, copying only the files that differ. With delete it removes what the other side does not have; a directory it cannot list stops it, and a sync down never writes through a local symlink. |
ghostd_install, ghostd_status |
Install ghostd on a target, and see that it answers. |
Local paths the tools take (output_file, to_path, from_path,
script_file, stdin_file, local_dir) are your own files, read and
written as you; a relative one is relative to the directory ghost mcp
started in. Paths on the target are relative to the home directory of the
user ghost logs in as.
ghost mcp keeps up to four SSH connections per target: calls share one,
and large transfers and syncs take their own, so a long upload does not hold
up a quick command. When a connection drops during a command, ghost
reconnects and asks for the same run again, which ghostd answers from the
run already going instead of starting it twice; an edit, rename, removal or
upload that was cut off is not repeated, and the tool says it may or may
not have happened.