Install & deploy
The one-command install and every other route to the same place - Homebrew, a clone, go install - plus the override knobs and the security posture you are accepting.
On this page
Installing Seamless is one command: a script downloads a release archive
containing the seamlessd daemon and seam CLI for macOS, Linux, or Windows,
verifies its SHA-256, and registers the daemon as a per-user service - launchd,
systemd, or a Scheduled Task - bound to loopback. No Docker, database server,
or Go toolchain. The Quickstart runs that command and moves on;
this page is what it did, and what to do when you want to steer it yourself.
Where everything lands - the service, logs, ports, and every path - is on
The service & where things live.
Install in one command
macOSLinux
curl -fsSL https://thereisnospoon.org/install | sh
Windows
The PowerShell installer does the same steps with a Scheduled Task in place of launchd/systemd:
irm https://thereisnospoon.org/install.ps1 | iex
This is the path for using Seamless (as opposed to working on it). The POSIX
script needs curl and tar; the PowerShell one needs nothing beyond Windows
itself. No Go toolchain is involved. In order, it:
- resolves the latest release and downloads the archive for your platform
(macOS, Linux, and Windows; amd64 and arm64), verifying its SHA-256
against the release's
checksums.txtbefore unpacking anything; whencosignis available it also verifies that manifest's keyless signature against this repository's release workflow identity; - installs
seamlessdandseaminto~/.local/bin; - runs
seamlessd install-hooks, which generates the bearer key into~/.config/seamless/seamless.yamlon first run, detects Claude Code, the Claude app chat surface, and Codex, and installs that set's hooks, MCP registrations, and skills; - installs and starts the per-user service - launchd
on macOS, systemd
--useron Linux, an at-logon Scheduled Task on Windows - and polls/healthzfor about 30 seconds until the daemon answers as the release it just installed.
That is the order of a first install. Once a config exists, steps 3 and 4 swap: the service restarts on the new build and must answer before the clients are rewired, so a failure while wiring them still leaves the new release serving.
Step 3 detects three install targets - Claude Code, the Claude app chat
surface (claude-desktop, the app's mcpServers bridge; it has no hooks or
skills), and Codex - and wires the detected set. That one selection
drives hooks, MCP registrations, and the maintained skills together -
seam-onboard always, seam-research only while its
optional feature is on. On a terminal
the run confirms the selection with a multi-select menu - answers are numbers or names, comma-separated
(1,3), defaulting to the detected set; headless, the detected set is wired
as-is. With nothing detected, a run on a terminal warns and asks whether to
install at all (defaulting to no), and a headless run aborts - the installer
never silently wires a client that is not there. SEAMLESS_NO_HOOKS skips the
detection along with the wiring, so a headless run with it never stops here.
Set SEAMLESS_CLIENT to make
the choice explicit: one target, a comma list, or all (every target the
platform can host - the chat surface exists only where the Claude app runs, so
all never fails on Linux over it). See Codex local setup for
the shared app/CLI/IDE profile and Codex's trust gate, and
Claude app chat setup for what the chat surface does and does
not get.
On every OS, Claude's copies live under $HOME/.claude/skills; Codex's live
under $CODEX_HOME/skills when set, otherwise $HOME/.codex/skills (the same
paths are %USERPROFILE%-relative on Windows). Invoke /seam-onboard in Claude
Code or $seam-onboard in Codex. The skill asks before adding its marked block
to global/project CLAUDE.md or AGENTS.md; it never silently edits either.
Windows
The Windows installer is per-user by the same principle as the others: it runs
as you, never elevates, and registers the Scheduled Task under your own
account (LogonType Interactive), so a single signed-in user is all it needs and
no administrator prompt ever appears. ~/.config/seamless and ~/.seamless
resolve under %USERPROFILE%, exactly the paths the daemon already searches.
Re-running it upgrades in place: binaries are swapped by rename (safe while the
daemon holds them open), the service restarts on the new build, and your config
and ~/.seamless are preserved. The selected clients are reconciled to those
new stable paths: owned stale hooks and the Codex stdio registration are
repaired, current definitions are untouched, foreign hooks are preserved, and
the recurring skill is refreshed. A machine whose config already says
role: client stays a client of its server_url - no knobs needed, and no
service registered. It is one shell
script with no dependencies to audit.
| Override | Effect |
|---|---|
SEAMLESS_VERSION=0.3.0 |
install that version instead of the latest |
SEAMLESS_INSTALL_DIR=~/bin |
put the binaries somewhere else |
SEAMLESS_CHECKSUMS_SHA256=<sha256> |
pin the release's checksums.txt to this SHA-256 (64 hex digits, either case): any other manifest is refused before anything is unpacked. Meant for a caller that has verified the manifest's signature itself (seamlessd's updater), so without cosign the run reports the signature as verified by seamlessd instead of warning |
SEAMLESS_CLIENT=claude|claude-desktop|codex|all |
choose which target(s) to wire instead of auto-detection; comma lists work (claude,claude-desktop) |
SEAMLESS_SERVER_URL=http://studio.local:8081 |
install as a client of a Seamless daemon running elsewhere: wire this machine's agent clients to that URL, install no service, and keep no data dir here. Requires SEAMLESS_MCP_API_KEY, and wins over the role and server_url an existing config names. See Share one daemon across a LAN |
SEAMLESS_MCP_API_KEY=<key> |
the server's bearer key, required alongside SEAMLESS_SERVER_URL - SEAMLESS_SERVER_URL without it is a hard error, because a client needs both. seamlessd client-config on the server prints the whole command |
SEAMLESS_NO_HOOKS=1 |
skip agent hooks, MCP registration, and skills, and the client detection that would choose them |
SEAMLESS_NO_ONBOARD_SKILL=1 |
skip the selected client(s)' one-shot onboarding skill |
SEAMLESS_NO_RESEARCH_SKILL=1 |
skip the selected client(s)' recurring research skill |
SEAMLESS_NO_SERVICE=1 |
install the binaries only; run seamlessd serve yourself |
SEAMLESS_ALLOW_ROOT=1 |
permit running as root (single-user containers) |
macOSLinux
Set them ahead of the shell, not the curl:
curl -fsSL https://thereisnospoon.org/install | SEAMLESS_VERSION=0.3.0 sh.
Windows
The same knobs are environment variables you set before the pipe -
$env:SEAMLESS_VERSION='0.3.0'; irm https://thereisnospoon.org/install.ps1 | iex
- with the one exception of
SEAMLESS_ALLOW_ROOT, which is POSIX-only (the Windows task is per-user by construction, so there is no root case to allow).
Everything here is per-user by construction - ~/.local/bin, ~/.config,
~/.seamless, a user service - so run it as yourself. Under curl | sudo sh it
would all land in root's home where your agents will never look, which is why
the script refuses root unless you insist.
Installing without the one-liner
The other routes end up in the same place with more of the steps left to you.
Homebrew
brew install arctop/tap/seamless
Every release publishes a cask to the arctop/homebrew-tap tap, on macOS and
Linux alike. It delivers the binaries only - seamlessd and seam on your
PATH, with the Gatekeeper quarantine attribute stripped on macOS (a fallback:
the binaries are notarized, but an offline first launch could still be
refused). It does not wire clients or register a service, so
finish with the two commands the cask's caveats print:
seamlessd install-hooks # bearer key on first run, hooks, MCP, skills
seamlessd serve # or set up the service yourself
Installed from the old 0spoon/tap? brew update will not move you (Homebrew
does not re-home a cask to a third-party tap on its own), so switch once:
brew uninstall --cask seamless
brew untap 0spoon/tap
brew install arctop/tap/seamless
From then on brew upgrade follows arctop/tap.
brew upgrade moves you to the latest release. The hooks keep working - they
resolve seam through brew's stable bin path - but restart the daemon
yourself so it runs the new build.
From a clone
make install # -> ~/.local/bin + ~/.config/seamless/seamless.yaml
make install PREFIX=/opt/seam # custom prefix (binaries land in $PREFIX/bin)
make uninstall # remove service, hooks, MCP, skills + binaries (data kept)
make install is the same destination from your own build, and it is macOS-only
(it renders the launchd plist from deploy/launchd/). It snapshots the binaries
and config to stable locations, then points launchd and the selected clients'
hooks/MCP definitions at the copies. Nothing live resolves through your working
tree, so make build, a branch switch, and a moved or cleaned repo cannot change
what the running daemon and every agent's hooks execute. Swapping them is make install, deliberately - Contributing covers using
it as the edit-test loop.
The config lands in ~/.config/seamless/, one of the paths Seamless already
searches ahead of ./seamless.yaml, so the hooks resolve it from any directory.
It is seeded only when absent - an install never clobbers a config holding
your bearer key. Delete it to re-seed.
On a role: client config, make install branches the same way the one-liner
does: it installs the binaries and wires the agent clients, prints the service
step as skipped naming the server, and polls that server's /healthz
instead of a local one - an unanswered server is a warning, not a failed
install. A client runs no daemon here, so rendering the plist would hand launchd
a job seamlessd serve refuses by design.
Go install and release archives
The remaining routes end up in the same place with less done for you:
go install github.com/arctop/seamless/cmd/...@latest needs Go 1.26+ and
reports the release it was installed at (seamlessd 0.6.0 (commit unknown, built unknown, source build) - the module proxy records no commit or build date), so
seamlessd update --check compares it like any release, though as a source
build it does not check for updates by itself; the
GitHub releases carry the same
prebuilt archives the installer fetches. From a bare binary, seamlessd serve
covers the essentials - first run seeds the config - and seamlessd install-hooks
wires the detected Claude Code/Codex clients; what you take on yourself is the
service.
The service
The installer registered seamlessd as a per-user service - launchd on macOS,
systemd --user on Linux, an at-logon Scheduled Task on Windows - and
seamlessd start|stop|restart|status controls it the same way everywhere. The
native commands, log locations, and every path Seamless touches are on
The service & where things live.
Upgrading
From v0.7.0, an install the installer made updates itself: each new release is
installed a day after it is published, at a moment no agent is using the daemon
(automatic updates). seamlessd update upgrades
in place to the newest release right now, on every OS - backed up, confirmed,
and rolled back if the new release does not come up. An install on v0.6.0 or
older has no update check: run seamlessd update once by hand.
Update & uninstall has the full procedure, the pinning knobs,
and how the release's installer and checksums are signature-verified first.
Uninstalling
seamlessd uninstall reverses the whole install and keeps your knowledge by
default. See Update & uninstall.
Security posture
What you are accepting when you run this:
- One static bearer key guards
/api/mcpand the console. Not JWT, not OAuth, no user accounts. It is a single-user local tool and the key is in your config file with0600permissions. - Default agent registrations do not copy that key. Claude Code calls
seam mcp-headersthroughheadersHelper; Codex launchesseam mcp-proxy. Both read the 0600 Seamless config at connection time, and neither puts the bearer value in client config or subprocess argv. A manual Codex direct-HTTP registration withhttp_headersdoes copy it intoconfig.toml; use that tradeoff deliberately. - Loopback bind by default (
127.0.0.1:8081). Nothing off your machine can reach it. Widening it is an explicit opt-in, and the daemon logs aSECURITYwarning every time it starts on a non-loopback address. - A Host-header allowlist guards against DNS rebinding: the daemon answers
the loopback names, a concrete bind host, the host of
server_url, and anything inallowed_hosts. Every otherHostgets421 Misdirected Request. A wildcard bind (0.0.0.0) with no host named anywhere is the one unguarded case - naming even one host arms the guard there too. - SSRF guards on capture.
capture_urlis the one tool that makes an outbound request on an agent's behalf, and its destination ports are restricted tocapture.allowed_ports(80 and 443 by default) - never "any port". - No product telemetry. Seamless sends no usage or analytics data. The
outbound traffic is: calls to a configured OpenAI or Anthropic provider (use
Ollama to keep model calls local), URLs an agent explicitly asks
capture_urlto fetch, the GitHub release check and download that an explicitseamlessd updateperforms, and - on a release build - the background update check: one anonymous request to GitHub's releases API every 6 hours, carrying no identifiers. On an install the installer made, an automatic update adds the same downloadsseamlessd updatemakes.update.check: falseturns both off and means no update traffic at all. - Release authenticity has two layers. Every installer verifies the
archive's SHA-256 against
checksums.txt. Whencosignis installed it also verifies the manifest's keyless signature against this repository's release workflow identity; without cosign it warns clearly and continues with checksum integrity only - unlessSEAMLESS_CHECKSUMS_SHA256pins the manifest to one its caller already verified, in which case any other manifest is refused and the run says the signature was verified by seamlessd.seamlessd update- and every automatic update - separately verifies the target release's installer script andchecksums.txtagainst their Sigstore bundles, in-process and by that release's exact tag, before executing anything, and pins the installer to thatchecksums.txt.curl | shstill means trusting the bytes served by the site, so read the script first if that boundary is not acceptable;go installlands in the same place.
Going beyond loopback, deliberately
A static bearer key is adequate because the listener is on loopback; the two
are a matched pair. Widen addr and that key becomes the only thing between a
network peer and your entire knowledge store - so widening it is three
deliberate steps, not one:
- Bind wide and say what you are called.
addr: 0.0.0.0:8081plusserver_url: https://studio.local:8081. The second key is what clients dial, and its host joins the allowlist, so the guard stays on. - Add TLS.
tls.cert_file+tls.key_filemakes the listener https (TLS 1.2 floor) and marks the console session cookieSecure. Without it the bearer key, every hook payload, and the whole console travel in the clear;seamlessd doctorreports that as a warning on thebindline. - Keep the blast radius small. There is still no rate limiting, no user accounts, and no per-client authorization: one key admits the whole corpus. A trusted LAN is the supported boundary. The public internet is not.
Share one daemon across a LAN is the walkthrough -
certificates, the seamlessd client-config pairing command, and what a remote
device does not get. If you only need to reach your own daemon from
elsewhere, a tunnel (Tailscale, SSH forwarding, Cloudflare Tunnel) over a
loopback bind is still the smaller change and the safer one.
See Configuration for every key.