A small command-line tool that creates a temporary, tokenized HTTP URL for a local file, so a long-running process or a coding agent on one machine can hand a clickable link to a phone, tablet, or another machine.
The motivating case: you're driving Claude on your home machine through
a remote-control session from your phone or another computer. Claude
has just rendered a video, generated a plot, finished a model, or dumped
an output to disk. You want to view it from the device you're on now.
you: let me see it
claude: ghosthost share ./renders/out.mp4
http://homepc.tail-4a9c2e.ts.net:8750/t/8f2b1c04e7a6/out.mp4
The link opens in any browser that can reach the host on the configured network (Tailscale by default), and stops working when the share's TTL expires — two hours after creation, by default.
Full-quality mp4: docs/demo-phone.mp4
The implementation is a single Go binary: a CLI in front of a background
HTTP daemon. Shares carry 128-bit tokens, auto-expire, and the daemon
binds to your Tailscale interface by default so URLs stay scoped to your
tailnet. A Claude skill (skills/ghosthost/SKILL.md) is included; it's
scoped to Claude Code remote-control sessions, where Claude runs
ghosthost share automatically when you ask to see a file. In other
session kinds the skill stays out of the way unless you ask for a
network-accessible link explicitly.
Status: v0.1. Windows and Linux run the full test suite on every CI push, including an end-to-end smoke test. macOS cross-compiles cleanly but is not currently exercised in CI.
- Video —
.mp4,.webm,.mov,.mkv,.avi,.m4v,.ogv,.mpg/.mpeg,.ts. Wrapped in an HTML<video>player, muted autoplay (so browsers honor it). - Audio —
.mp3,.wav,.flac,.m4a,.aac,.ogg/.oga,.opus. Wrapped in an HTML<audio>player, autoplay, not muted. - Images —
.png,.jpg/.jpeg,.gif,.webp,.avif,.svg,.bmp,.ico. Rendered inline by the browser. - Documents and text —
.pdf,.txt,.log,.md,.json,.csv,.yaml/.yml,.html/.htm,.xml. Served inline; the browser decides how to display. - Anything else — served as a normal download.
Append ?dl=1 to any URL to force Content-Disposition: attachment and skip the inline player/viewer.
- A coding agent has generated a video, plot, image, or scraped page on a remote machine and you want to view the output without copying files around.
- A long-running ML job has produced a generated image, training-loss plot, audio clip, or checkpoint sample on your GPU machine.
- A browser-automation or scraping job has dropped screenshots, PDFs, or CSVs on disk.
- The file is on your desktop, you're on your phone, and you'd rather not set up scp, a throwaway HTTP server, or a Dropbox round-trip.
Copy skills/ghosthost/SKILL.md into the Claude skills directory and restart Claude:
# macOS/Linux
mkdir -p ~/.claude/skills/ghosthost
cp skills/ghosthost/SKILL.md ~/.claude/skills/ghosthost/SKILL.md# Windows
%USERPROFILE%\.claude\skills\ghosthost\SKILL.md
The skill is scoped to Claude Code remote-control (bridge) sessions
— the kind launched via claude remote-control, where Claude detects
CLAUDE_CODE_ENVIRONMENT_KIND=bridge in its environment. In bridge
sessions, Claude reaches for ghosthost share on its own when you ask
to see a file. In plain desktop / CLI sessions Claude leaves the skill
alone unless you (a) explicitly name ghosthost, (b) explicitly ask
for a network-accessible URL (e.g., "give me a link my phone on this
wifi can hit", "share it on the tailnet", "URL I can paste in Slack"),
or (c) are smoke-testing the install. This keeps the skill from firing
on unrelated file-path mentions when you're sitting at the host.
See CLAUDE.md for the full end-to-end setup and smoke-test checklist.
See INSTALL.md for the full guide with signature verification, upgrade/uninstall paths, and platform-specific notes.
go install github.com/godspede/ghosthost/cmd/ghosthost@latestRequires Go 1.25+ (per go.mod). Prebuilt binaries for Windows, Linux, and macOS will ship through goreleaser — see the Releases page.
First invocation writes a template config and exits with a friendly error. Default paths:
- Windows:
%APPDATA%\ghosthost\config.toml - Linux/macOS:
~/.config/ghosthost/config.toml
Override with --config <path> on any command.
host = "homepc.tail-4a9c2e.ts.net"
bind = "tailscale" # or an explicit IP, or "0.0.0.0"
port = 8750
admin_port = 8751
data_dir = "C:\\Users\\you\\AppData\\Local\\ghosthost"
default_ttl = "2h"
idle_shutdown = "30m"On first run, ghosthost shells out to tailscale status --json and pre-fills host with your MagicDNS name when available. bind = "tailscale" fails fast at daemon start with a clear error if the tailscale CLI is missing or not logged in. bind = "0.0.0.0" exposes the server on every interface the host joins; the daemon logs a warning at startup whenever that's set.
Plain HTTP is fine on a trusted tailnet. For HTTPS, point tls_cert and tls_key at PEM files:
tls_cert = "/path/to/cert.pem"
tls_key = "/path/to/key.pem"If both are set, the public server uses TLS and ghosthost share returns https:// URLs. Set only one and the daemon refuses to start.
For Tailscale users, tailscale cert <your-magicdns-name> produces a browser-trusted cert/key pair via Tailscale's Let's Encrypt integration. Point the two config keys at those files and you're done. Rotating the cert is just rewriting the files — the daemon reloads them on restart because http.Server.ServeTLS re-reads the paths each time it starts.
The admin API on 127.0.0.1 stays plain HTTP regardless; TLS adds nothing on a loopback socket.
| Command | What it does |
|---|---|
ghosthost share <path>... [--ttl 2h] [--as name] [--anon] [--verbose] [--yes] |
Create one or more shares, print one URL per line. Pass --verbose for the full id + expiry block. |
ghosthost info <arg> |
Look up an active share by full URL, URL path, bare token, or bare id. |
ghosthost list |
Active shares. |
ghosthost history [--limit N] |
All historical share events. |
ghosthost reshare <id> |
Issue a fresh URL for a prior share. |
ghosthost revoke <id> |
Stop serving immediately. |
ghosthost status |
Daemon liveness. |
ghosthost stop |
Shut the daemon down (it will auto-spawn again on next use). |
Add --json to any command for machine-readable output. The daemon auto-spawns on first use and self-exits after idle_shutdown (30 min default) with no active shares.
Human-mode share prints one URL per file, in argv order. Pass --verbose for the full id + expiry block, or look up an active share later with ghosthost info <url-or-id>.
Pass multiple paths in a single invocation — one URL per line, argv order:
$ ghosthost share a.png b.png c.png
http://homepc.tail-4a9c2e.ts.net:8750/t/abc.../a.png
http://homepc.tail-4a9c2e.ts.net:8750/t/def.../b.png
http://homepc.tail-4a9c2e.ts.net:8750/t/ghi.../c.png
- Atomic validation — any bad path aborts the whole batch before any share is created, and all errors are reported together.
--asrequires exactly one file; using it with multiple paths is an error.- Batches over 64 files require
--yesto confirm.
Pass --anon to replace each URL's filename segment with a random 6-char base32 slug while preserving the file extension, so the recipient's browser handles the download correctly:
$ ghosthost share --anon secret-tax-return.pdf
http://homepc.tail-4a9c2e.ts.net:8750/t/abc.../k9vm3q.pdf
--anon works for single-file and multi-file invocations alike.
Breaking:
ghosthost share --jsonnow always emits a JSON array. Single-file invocations return a one-element array. Callers parsing stdout must update to readresult[0](e.g.jq '.[0].url').
ghosthost info retrieves metadata for any currently-live share. Pass any of: the full URL, just the URL path (/t/<token>/<name> or t/<token>/<name>), the bare token, or the bare id.
$ ghosthost info http://homepc.tail-4a9c2e.ts.net:8750/t/k3n.../hello.txt
URL: http://homepc.tail-4a9c2e.ts.net:8750/t/k3n.../hello.txt
ID: 8f2b1c04
Src: /home/you/hello.txt
Created: 2026-04-21T13:14:15Z
Expires: 2026-04-21T15:14:15Z (1h59m59s from now)
Expired, revoked, or unknown shares exit with code 5 and a "not found" message — no information about which tokens ever existed is disclosed.
Tailscale is the default transport, but nothing about the binary is hard-wired to it. The URL the daemon prints is https://<host>:<port>/s/<token>/<name>, and any transport that lands a reachable host at the daemon's listening interface works: your LAN IP, another VPN (WireGuard, Nebula, ZeroTier), or a public reverse proxy or tunnel terminating TLS in front of the daemon. Set host and bind accordingly. See CLAUDE.md for concrete recipes.
- Tokens are 128 bits from
crypto/rand, compared in constant time. Only their SHA-256 digests are written to disk (history.jsonl). - The admin API is bound to
127.0.0.1only and authenticated by a per-daemon bearer secret stored in an ACL-restricted lockfile. - Shares expire after
default_ttl(2h default); revocation is immediate. - Tokens in the URL are the only authentication on the data-plane. Public exposure is at your own risk.
See SECURITY.md for the full threat model.
Windows is the primary target. All the Windows-specific hardening lives in-tree: LockFileEx on the daemon lockfile, reparse-point rejection when resolving share paths, detached-process flags on daemon spawn. Linux and macOS binaries build cleanly and the core flows work; full cross-platform parity is still in progress for v0.1.
Every JSON response from the CLI and admin API includes a "schema_version" field. Within a major version, schemas are append-only. Breaking changes require a major version bump.
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | generic error |
| 2 | usage error |
| 3 | config missing or invalid |
| 4 | daemon unreachable after spawn |
| 5 | id not found |
| 6 | source path invalid or missing |
MIT.