seamless docs

seamlessd CLI

The daemon and operator CLI - serve, doctor, import, install-hooks, uninstall, update, map-repo, family, console-open, start/stop/restart/status, and version.

seamlessd is both the server and the operator CLI. serve runs the daemon; every other subcommand is a one-shot that opens the same config and database directly, without going through a running server. That means most of them work whether or not the daemon is up - and that map-repo and family write state the running daemon reads.

Each subcommand parses its own flags. None of them take positional arguments except family, which takes only positionals.

For the keys every command below resolves, see Configuration.

seamlessd serve

seamlessd serve [--addr HOST:PORT]

Starts the HTTP server and blocks until SIGINT or SIGTERM, then shuts down gracefully. --addr overrides the configured bind address (default 127.0.0.1:8081).

On a true first run - no config file anywhere in the search order and no SEAMLESS_MCP_API_KEY in the environment - it generates the bearer key and writes it to ~/.config/seamless/seamless.yaml before starting. An existing config file is never edited, even when its key is empty.

It wires up:

  • /healthz - liveness plus a database ping. Reports degraded with a 503 when the ping fails.
  • /api/mcp - the MCP tool endpoint, bearer-authenticated.
  • /api/hooks/... - the session and plan-capture hooks.
  • /console/... - the observability console. The bare root / redirects here.

Startup is deliberately tolerant of a half-configured install, and the log is where you find out:

  • No embedder - recall degrades to FTS-only for the life of the process. A missing credential logs a warning; a malformed setting (a bad base_url) logs an error, because that one is a typo rather than a choice.
  • No chat client - gardener digest passes no-op.
  • Empty mcp.api_key (a config file exists but leaves it blank) - logs a warning, and every MCP and hook request is then rejected.
  • Gardener disabled - logged, and no maintenance passes run.

The startup line carries the version, commit, and data directory, which is how you spot a daemon running older code than your working tree.

seamlessd doctor

seamlessd doctor

Server-side self-checks. Each line reports ok, warn, or fail; only a fail exits non-zero - warnings are informational. Checks stop early if config or the database cannot be loaded at all.

Check What it reports
binary The version that ran.
config Which file it loaded, or that it fell back to defaults + env.
data_dir The resolved data directory.
mcp.api_key Set, or a warning that /api/mcp will reject everything.
llm The provider, or a warning that its credential is missing.
embedder Probes the embedder with a real embed call. Unreachable, unconfigured, or provider anthropic (no embeddings API) is a warning: recall degrades to FTS.
database Path, schema version, and table count. Opens and migrates if needed.
mcp_tools Fails if the number of registered tools disagrees with the expected count - catches a tool written but never wired in.
claude CLI runtime / claude app runtime Each discoverable Claude Code runtime's self-reported version, separately: the PATH CLI and, on macOS, every runtime the desktop app has retained - they can differ, and collapsing them would hide exactly that skew. No discoverable runtime means no lines.
hooks Claude Code definitions compared with today's desired profile.
claude desktop mcp The chat surface's claude_desktop_config.json entry compared with the desired stdio bridge. Absent is an info line naming the opt-in command, never a nag; an exact entry reports OK while stating that the running app's loaded state is unverifiable (the app reads the config at startup). No lines when neither the app nor a desktop config exists.
codex CLI runtime / codex app runtime Each discoverable Codex runtime's self-reported version, separately, on the same principle.
codex hooks Codex current, stale, and missing definitions, including command targets.
codex hook trust Always warns that trust is unverified and directs you to Codex /hooks; no private trust state is read.
codex hook activity Last observed SessionStart/UserPromptSubmit event, if any; evidence only, not proof of current trust.
codex mcp Exact enabled stdio bridge state from codex mcp get seamless --json, plus executable/config target existence.
gardener The ticker configuration, or a warning that it is disabled.

The definition checks compare current desired state, not mere existence. The shared classifier recognizes exact current definitions, marked stale entries, and only unmistakable legacy Seamless shapes; arbitrary foreign hooks survive. Codex trust is a separate fact because Codex exposes no supported query for the current trust decision. A recent hook observation cannot make a changed command healthy.

Reach for it after changing config, after an upgrade, or as the first step when recall has quietly gone lexical.

seamlessd import

seamlessd import [--from DIR] [--skip LIST] [--embed=false]

Imports a Seam v1 data directory into this instance. Memory and note files are written and indexed under the v2 data directory; trials, sessions, and tool-call events are inserted into the database.

Flag Default Meaning
--from ~/.seam v1 data directory to import from. A leading ~ expands.
--skip briefings Comma-separated storage projects to skip.
--embed true Embed imported items for cosine search, using the configured provider.

It is idempotent by id, so re-running imports only what is new - which makes a delta re-import safe after the first pass. It honours SIGINT/SIGTERM, and prints a report even when the import ends in an error. With --embed on and no usable embedder, it warns and imports without vectors rather than failing.

seamlessd install-hooks

seamlessd install-hooks [--client claude|codex|claude-desktop|all|detect] [--settings PATH] [--codex-hooks PATH] [--desktop-config PATH] [--url BASE] [--seam PATH] [--mcp=false] [--skills=false]

Wires the selected install target(s) to Seamless: merges the hook entries into each hook client's file (Claude Code settings.json, Codex hooks.json), registers the MCP server (via the client's CLI, or for the Claude app chat surface by editing claude_desktop_config.json directly), and installs the embedded seam-onboard and seam-research skills into each hook client's skill home. The claude-desktop target is the chat surface's MCP bridge only - it has no hooks and no skills - so selecting only it together with --mcp=false is an error rather than a silent no-op.

Flag Default Meaning
--client detect Which target(s) to wire: claude, codex, claude-desktop, a comma list of those (claude,claude-desktop), all (every target this platform can host), or detect (the targets present on this machine). With the flag omitted on a terminal, a multi-select menu prompts, defaulting to the detected set; non-interactive runs detect without prompting. With nothing detected, detect is an error, never a silent Claude Code default.
--settings ~/.claude/settings.json Target Claude Code settings file, created if absent. Point it at a project-scoped .claude/settings.json to scope the hooks to one repo.
--codex-hooks $CODEX_HOME/hooks.json, else ~/.codex/hooks.json Target Codex hooks file, created if absent.
--desktop-config the app's per-OS location Claude app claude_desktop_config.json to register the chat-surface bridge in (macOS ~/Library/Application Support/Claude/, Windows %APPDATA%\Claude\).
--url derived from the config addr Base URL of the daemon.
--seam sibling of this binary, else seam on PATH Path to the seam CLI baked into the command hooks.
--mcp true Register the MCP server via the client CLI (claude mcp add-json --scope user / codex mcp add).
--skills true Install the embedded skills for each wired client. A failure here degrades to a warning - skills must not cost the daemon bootstrap.

It generates mcp.api_key on a true first run under the same rule as serve, and refuses to run when an existing config leaves the key empty, since the key is what the hooks authenticate with. The loaded config path is made absolute and passed to command hooks as --config, so they resolve config from any working directory. A --seam binary that cannot be found is a printed warning, not an error - the hooks would fail at fire time, so it says so now.

The hook file is written before MCP registration. Claude Code registration stays best-effort because its current CLI exposes no machine-readable state: a missing CLI or failed mcp add-json prints the exact command to run yourself. Codex is stricter. It decodes codex mcp get seamless --json, leaves an exact enabled stdio bridge unchanged, repairs an owned disabled/stale bridge with mcp add, and re-reads it before reporting success. A direct-HTTP or other incompatible entry under seamless is not overwritten; remove it explicitly or rerun with --mcp=false to keep that manual transport.

The claude-desktop target has no management CLI at all, so its registration is a merge-preserving edit of claude_desktop_config.json: the reserved seamless entry is set to the seam mcp-proxy stdio bridge (absolute paths, no secret - the bridge reads the bearer key from Seamless's config), every foreign key round-trips byte-for-byte, the file is backed up once before the first change, and the write is verified by re-reading it. An incompatible entry under the reserved name is an error naming the in-app fix (Settings > Developer > Edit Config), and every change ends with a restart notice - the app reads the file only at startup. See Claude app chat setup.

The hook merge preserves unknown keys and foreign entries, replaces marked stale definitions, adopts only recognizable legacy Seamless URLs/commands, deduplicates owned entries, and backs the file up once before the first change. An arbitrary executable merely containing hook <event> is foreign. An already-current file is reported as up to date and left untouched. Each event reports added, updated, adopted, deduped, or unchanged.

For Claude Code, six events are installed together: SessionStart, UserPromptSubmit, SessionEnd, PostToolUse, SubagentStop, and PermissionRequest. All are command hooks that run seam hook <event> (exec form, no shell) except UserPromptSubmit, which is an http hook - Claude Code will not run an http hook for SessionStart at all, and at SessionEnd a fire-and-forget request races process teardown, so the findings harvest would often be lost. The Codex profile is five shell-string command hooks, including safe constraint injection and parent-only lifecycle handling for subagents; the hooks reference has both tables.

seamlessd uninstall

seamlessd uninstall [--client claude|codex|claude-desktop|all|detect] [--dry-run] [--yes] [--purge]

Reverses a full install on any supported OS: stops and removes the per-user service, strips the Seamless hook entries, deregisters the MCP server (including the chat surface's claude_desktop_config.json entry - only the reserved seamless key is removed, everything else stays byte-for-byte), removes the installed skills, and deletes the binaries. Config and the data dir (~/.seamless - memories and notes are markdown that outlive the program) are kept unless --purge is passed. Every external step is best-effort: an already-gone file or a missing client CLI is a note, never a failure, so uninstall is idempotent and safe to re-run.

Hook removal uses the installer's same classifier: current, marked-stale, and recognizable legacy Seamless definitions are removed; foreign definitions are preserved. Skill removal is scoped to seam-onboard, seam-research, and the one-shot delivery marker in each selected client's skill root.

Flag Default Meaning
--client all Which target(s) to remove hooks/MCP/skills for: claude, codex, claude-desktop, a comma list, all, or detect. claude-desktop scopes the run to the chat surface's desktop-config entry.
--dry-run false Print what would be removed and exit without changing anything.
--yes false Skip the confirmation prompt.
--purge false Also delete the config dir (~/.config/seamless) and data dir (~/.seamless).
--settings ~/.claude/settings.json Claude Code settings file to remove hooks from.
--codex-hooks $CODEX_HOME/hooks.json, else ~/.codex/hooks.json Codex hooks file to remove hooks from.
--desktop-config the app's per-OS location Claude app claude_desktop_config.json to remove the chat-surface bridge from.
--url derived from the config addr Base URL the hook entries were installed with.
--install-dir $SEAMLESS_INSTALL_DIR, else ~/.local/bin Directory the binaries were installed to.
--mcp true Also deregister the MCP server (claude/codex mcp remove).

seamlessd update

seamlessd update [--check] [--dry-run] [--url URL]

Upgrades Seamless in place to the latest published release by re-running the canonical installer for this OS - the same script a fresh curl ... | sh / irm ... | iex install runs, fetched from the latest GitHub release's assets. There is deliberately no second upgrade implementation: after verification, update pipes that script to sh (or powershell), so binaries are swapped by rename, the service restarts on the new build, and hooks are reconciled exactly as Install & deploy describes for a re-run.

Before anything executes, the fetched script is verified against the Sigstore bundle published alongside it - proof the bytes came out of this repository's release workflow on a version tag, not merely from the right host. Verification failure is fatal, with no fallback. The fetch is HTTPS-only, including every redirect hop.

Flag Default Meaning
--check false Report installed vs latest release version and exit without changing anything.
--dry-run false Print the source URL, signature status, and the equivalent hand-run one-liner, without fetching or executing.
--url the latest release's installer asset Override the installer URL. A custom URL carries no Sigstore bundle, so it runs TLS-only with a printed warning.

The installer's env knobs pass through: SEAMLESS_VERSION=0.3.0 seamlessd update pins a version, SEAMLESS_INSTALL_DIR=... retargets, exactly as the curl installer does.

seamlessd map-repo

seamlessd map-repo --project SLUG [--path DIR]

Adds an entry to the repo_project_map setting, so an agent whose working directory is under that path resolves to that project - in the hooks and in session_start. This is what makes a briefing arrive scoped to the right project without the agent passing project anywhere.

Mostly you will not need it: a git repo maps itself on its first session, taking the slug from the repo root's directory name. Run map-repo to override that derived slug, or to map a directory that is not a git repo.

--project is required. --path defaults to the current directory and is made absolute. The command also ensures the project exists, so mapping a new slug registers it. Writes straight to the database; no running daemon needed.

seamlessd family

seamlessd family list
seamlessd family add <name> <slug> [<slug>...]
seamlessd family remove <name> [<slug>...]

Manages the project_families setting: named groupings whose members surface each other's recent findings in briefings. Use it when two projects are really one body of work and an agent in either should see what happened in the other.

Members are project slugs, not repo paths - resolve a repo to its slug with map-repo first. remove with no slugs removes the whole family; with slugs it removes just those members. rm is accepted as an alias for remove.

Adding a slug that is not yet a registered project prints a warning but succeeds: the membership starts taking effect once an agent opens that repo and registers it.

seamlessd console-open

seamlessd console-open [--browser APP]

Opens the console in a browser, already authenticated. It renders a one-shot self-submitting login page to a 0600 temp file and opens it; the page POSTs the static key to the console's login endpoint, which sets the session cookie and redirects into the console - so you land on an authenticated page without pasting a key.

--browser targets a specific browser application (for example "Google Chrome", so an agent driving Chrome gets the auth cookie even when another browser is the default). It is macOS only and is rejected with an error on other platforms rather than silently opening the default browser.

It refuses to run when mcp.api_key is empty, or when the server does not answer /healthz within two seconds - the page has nowhere to POST otherwise. Any HTTP response counts as reachable, including a degraded 503.

seamlessd start / stop / restart / status

seamlessd start      # or: stop | restart | status

Control the installed background service without remembering each platform's service manager. start, stop, and restart act on the LaunchAgent (macOS), the systemd --user unit (Linux), or the Scheduled Task (Windows); status prints that manager's own state output.

These control an already-installed service - they do not create one. If it was never installed, they exit with a hint to run the installer (or make install from a clone) rather than a cryptic launchctl/systemctl/schtasks error.

restart is in-place and fast (on macOS launchctl kickstart -k, falling back to a fresh load if the job was unloaded). stop fully stops the service - on macOS the LaunchAgent has KeepAlive, so this unloads it rather than letting it be resurrected. Idempotent no-ops - starting a running service, stopping a stopped one - are reported as a note, not a failure.

From a clone, make start / stop / restart / status wrap these exactly.

seamlessd version

seamlessd version

Prints the version, commit, and build date. -v and --version are aliases.

Commit and build date are link-time metadata set by the Makefile; a plain go build leaves them unknown. The same version string appears in /healthz, the MCP handshake, and the startup log - compare them when you suspect the daemon is running older code than what you just built.