Skip to content

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.

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.

ghost ghostd install web-1

ghost 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-1

When 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.

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 /etc
ghost x web-1 --cwd /srv/app -- git log -1 --format='%H %s'
ghost x web-1 --env RUST_LOG=debug --timeout 600 -- cargo test
ghost x web-1 --stdin -- psql app < query.sql
ghost x web-1 --script ./deploy.sh -- --fast

The 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).

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 8080
ghost ghostd job list web-1
ghost ghostd job logs web-1 JOB_ID --follow
ghost ghostd job stop web-1 JOB_ID

A 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.

ghost ghostd put web-1 ./release.tar.gz /srv/app/release.tar.gz
ghost ghostd get web-1 /var/log/app.log ./app.log

Both check the file’s SHA-256 at the end, and a file is replaced atomically: a reader never sees half of it.

ghost mcp is an MCP server over stdio. Add it to Claude Code:

claude mcp add ghost -- ghost mcp

or 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.