The full command surface for first-tree. Every command listed here is in
the shipped binary — first-tree --help (and first-tree <namespace> --help) are the canonical source of truth, this document is a
human-friendly index over them.
Keeping this file current. Any PR that changes the command surface (adds, renames, removes, or re-flags a verb / namespace) must update this file in the same PR. The grep checks that gate
Forbid legacy CLI / env namesonly catch a handful of retired identifiers; the broader "what commands exist and what do they do" contract is enforced by humans against this document.
Production:
curl -fsSL https://download.first-tree.ai/releases/prod/install.sh | sh
~/.local/bin/first-tree login <connect-code>Staging:
curl -fsSL https://download.first-tree.ai/releases/staging/install.sh | sh
~/.local/bin/first-tree-staging login <connect-code>The public shell installers support macOS and Linux and bundle Node.js. They
install channel-specific binaries under ~/.local/bin: first-tree / ft
for production and first-tree-staging / fts for staging. The full path in
the login command works immediately, before the current shell reloads PATH.
The two lines are intentionally independent and do not provide shell-level
transaction protection: when pasted together, an install-line failure does not
automatically prevent the login line from running, and POSIX sh does not
guarantee that curl | sh preserves a curl failure status.
For self-hosted deployments, use the two-line command returned by the web
console. It includes the server and portable download-base overrides when
needed. Development builds continue to use scripts/dev-install.sh and
first-tree-dev login <connect-code>.
| Flag | Effect |
|---|---|
--json |
Emit only machine-readable JSON on stdout; silence human status lines on stderr. |
--verbose |
Raise the log level to debug (overrides FIRST_TREE_LOG_LEVEL). |
--version |
Print the CLI version and exit. |
--help |
Print help for the command or namespace. |
first-tree
├── login <code> Sign this computer in or switch local clients
├── logout Stop the daemon and clear credentials
├── computer ... Computer-level local state recovery
├── status CLI + daemon + server + auth + agent overview
├── doctor Cross-subsystem readiness check
├── upgrade Self-update + restart the daemon
├── agent ... Agent management (config, bindings, sessions, messaging)
├── chat ... Chats and messaging (create, send, list, history, open)
├── doc ... Org document library (publish, comments, reply, resolve, status)
├── org ... Organization-level operations
├── daemon ... Background daemon (start, stop, status, doctor, probe)
├── config ... View/modify this machine's client.yaml
└── tree ... Validate and browse Context Trees
first-tree login <code> [--no-start] [--force-switch]
Sign this computer in using a short connect code from the web console. New
codes are exchanged against this CLI channel's default server URL
(first-tree → production, first-tree-staging → staging, first-tree-dev →
local dev), with FIRST_TREE_SERVER_URL as an explicit override for custom
deployments. Connect URLs are not accepted; only legacy JWT tokens with an
iss claim remain accepted during rollout.
If this machine already has credentials for another user, login asks for
explicit confirmation and switches the active local client after stopping and
draining the old runtime. In non-TTY automation, --force-switch is the only
confirmation flag; it does not skip supervisor, drain, filesystem, or journal
safety checks. If credentials are missing, login preserves client.yaml and
local agent state so the same user can reconnect after a normal logout.
| Flag | Effect |
|---|---|
--no-start |
Write credentials and exit without installing/starting the background daemon. |
--force-switch |
Confirm a different-user local client switch in non-interactive mode. Safety gates still run. |
first-tree logout [--purge]
Stop the daemon and clear credentials. --purge additionally removes active
root client state, parked clients under $FIRST_TREE_HOME/parked-clients/, and
switch lock/journal files. This is a destructive local reset path, not the
normal account-switch path. To switch this computer to another First Tree user,
run first-tree login <code> with the new user's connect code and confirm
the switch. Before deleting local state, --purge retires the current server
client so it disappears from default Computers views and cannot be reactivated
with the same client id. Retiring is destructive for runtime routing on that
client: non-deleted agents pinned to it are suspended and unpinned, while agent
identity, chats, history, and profile data remain; those cleared agents can be
moved back onto a connected computer/runtime from the Agent Runtime tab. Logout
stops both the background service and any live daemon start --foreground
runtime markers for the active client before clearing credentials/state. If the
daemon is active and cannot be stopped, a foreground runtime cannot be stopped,
or the server-side retire fails, --purge refuses to delete local client state.
The default keeps local client/agent state for the same user to reconnect later.
Computer-level local state recovery.
first-tree computer
└── reset
first-tree computer reset
Stop the daemon and remove active root client state, parked clients under
$FIRST_TREE_HOME/parked-clients/, switch lock/journal files, and active
credentials. Use this when local identity state is damaged or when you
intentionally want to discard every local First Tree client stored in this
installation. This is local-only and does not retire server client rows. Normal
different-user switching should use
first-tree login <code> instead, which parks inactive clients.
first-tree status
Single-screen overview: CLI version, daemon state, server reachability, auth health, and the agents this client manages.
first-tree doctor
Cross-subsystem readiness check covering the daemon, server reachability,
WebSocket, and configured agents. Use this when status flags something
red and you want a guided drill-down.
first-tree upgrade [--check] [--no-restart]
Self-update for the CLI: query the configured server for its recommended
Command version when a server URL is configured, install that exact version
through the current install mode, refresh the supervisor definition on top of
the new bits, then restart the client service. If no server URL is configured
yet, upgrade falls back to the current channel's latest release data directly
so the update path still works before login/config. Portable installs download
the channel manifest and verified tarball, including the bundled Node.js
runtime. Existing npm-mode installs retain their package-manager update path
and continue using the system Node.js runtime.
| Flag | Effect |
|---|---|
--check |
Only check for an available version; print "update available" or "already on latest". Do not install. |
--no-restart |
Install the new version and refresh the supervisor definition, but leave the running service alone. Used for staged rollouts. |
Refusing to run from a source checkout (anywhere under a .git
ancestor) is intentional — it keeps a dev build from accidentally overwriting
a hosted-channel installation. For local development use
scripts/dev-install.sh (see docs/development/local-dev-isolation.md).
For an existing npm-mode installation, upgrade checks the target package's
engines.node metadata before install when npm can provide it. If the target
requires a newer Node.js than the current process is running, the command fails
before install with a system-Node upgrade hint and a shell-installer migration
hint. npm-mode updates do not replace Node.js themselves.
Agent management — local config, bindings, sessions, messaging debug helpers.
first-tree agent
├── list [--remote] [--org <id>]
├── add --agent-id <uuid>
├── create <name> --type <t> --client-id <id> [--runtime <r>] [--display-name <s>] [--org <id>]
├── remove <name>
├── prune [--yes] [--dry-run]
├── status [name]
├── reset <name>
├── config <subcommand>
├── bind <subcommand>
├── workspace <subcommand>
└── session <subcommand>
first-tree agent list # locally-configured agents on this client
first-tree agent list --remote # every agent the signed-in user manages on the server
first-tree agent list --remote --org <id> # cross-org view (multi-org operators)
first-tree agent create <name> --type <human|agent> --client-id <thisClient> [--runtime claude-code|codex]
Creates the agent row on the server and binds it to the given client
machine. The local agents/<name>/agent.yaml is written by the running
daemon via the server-pushed agent:pinned frame; no second command
needed if the daemon is already up.
first-tree agent add --agent-id <uuid>
Register an existing server-side agent on this client. Use this when the daemon was not running at the moment the agent was pinned, or when moving an agent to a second computer that's already signed into the same user.
first-tree agent remove <name> # delete local config dir, workspace, session state
first-tree agent prune [--yes] [--dry-run] # remove every local alias the server no longer pins to you
prune is the counterpart to daemon doctor's "stale aliases" warning.
first-tree agent status # all agents this client manages
first-tree agent status <name> # one agent's runtime view from the server
first-tree agent reset <name> # reset agent error state to idle
Mutate the agent's server-side runtime configuration (model, prompt,
MCP servers, env, repos). Edits the agent_configs.payload JSONB row
through the Admin API.
first-tree agent config
├── show <agent>
├── set-model <agent> <model> # alias: opus | sonnet | haiku, or full id (e.g. claude-opus-4-7)
├── set-reasoning-effort <agent> <level>
├── prompt show <agent> [--raw] # per-agent prompt fragment; --raw is verbatim (round-trippable)
├── prompt set <agent> [-f <file>] [--force] # replace the fragment ONLY; reads stdin if no file.
│ # Rejects copies of the assembled AGENTS.md (generated marker /
│ # briefing headings); --force overrides the heading heuristic.
│ # Does NOT cover inline replacements of team prompts — those are
│ # resource bindings, managed in Cloud → Org Settings → Resources.
├── append-prompt <agent> [-f <file>] # deprecated alias of `prompt set`
├── add-mcp <agent> --name <id> --transport <t> [--command <c> --args <a>... | --url <u>]
├── set-env <agent> KEY=VALUE [--sensitive]
├── add-repo <agent> <url> [--ref <branch>] [--path <local>]
└── dry-run <agent> -f <patch.json> # validate + diff, no persist
Reasoning effort values are provider-specific. Claude Code and Claude Code
TUI accept "" (inherit the operator's local setting), low, medium,
high, or max. Codex accepts low, medium, high, xhigh, max, or
ultra; availability of the higher levels is model-dependent and rejected
combinations are reported by the provider.
first-tree agent bind
└── client <agentName> --client-id <id> # first-time bind only; later moves use managed runtime switch
first-tree agent workspace clean [agent-name] [--ttl <days>]
Remove stale workspace directories (older than the TTL with no active session). Without an agent name, sweeps every local agent.
first-tree agent session
├── list <agent-name> [--state <active|suspended|evicted|errored>]
├── suspend <agent-name> <chat-id>
└── terminate <agent-name> <chat-id>
Day-to-day messaging.
first-tree chat
├── create [message] # create a separate task chat and write its first message
│ --to <name> # initial recipient to mention + wake; repeatable, required
│ --with <name> # context participant; added silently, not woken by the first message
│ --topic <text> / --description <text> # initial chat self-description
│ --request # first message is a tracked ask; the body IS the ask, decision-self-sufficient (why + recap + question + recommendation); exactly one --to human
│ --options <json> / --multi-select # (with --request) 2–4 options {label,description,preview?}; allow multi-pick
├── send <name> [message] # wake a participant — agent or human (a send to a human is informational only; a question the next step depends on goes through `chat ask`)
│ # body: [message] arg, or stdin (omit [message]), or -F <path>; prefer stdin/-F for rich bodies (shell-safe)
│ -F, --message-file <path> # read the body from <path> (`-` = stdin); content never hits the shell
│ --reply-to <messageId> # thread a reply under a message (pure threading)
├── ask <name> [message] # ask a HUMAN a tracked question; the body IS the ask, decision-self-sufficient (why it exists + recent-context recap + question + recommendation)
│ # body: [message] arg, or stdin (omit [message]), or -F <path>; prefer stdin/-F for rich bodies (shell-safe)
│ -F, --message-file <path> # read the body from <path> (`-` = stdin); content never hits the shell
│ --options <json> # 2–4 answer options {label (1–5 words), description, preview?}; omit for free-text
│ --multi-select # allow picking more than one option (requires --options)
│ # always a fresh top-level question — no threading, and no resolve flag
│ # (the human answers in the web UI; an agent can only ASK)
├── invite <agentName> # add to FIRST_TREE_CHAT_ID before same-task send
├── list
├── history <chatId>
├── update # update topic and/or description (each independently)
│ --topic <text> / --clear-topic # set/clear the short display label
│ --description <text> / --clear-description # set/clear the work summary + status report (Markdown; `-` = read from stdin/heredoc)
│ --chat <chatId> / --agent <name> # target another chat / the named agent
├── set-topic [topic] # [DEPRECATED — use `update`] hidden alias
└── open <agent-name> # interactive REPL
# Split off separate work into a new task chat and write the first message.
# --to recipients are mentioned and woken; --with participants are added for
# context but receive only silent initial history. This is not an empty-chat or
# same-task handoff tool.
first-tree chat create "Please review the rollout plan." --to code-agent --with reviewer-agent \
--topic "rollout review" \
--description "reviewing rollout plan; waiting on code-agent"
# Start a new task chat with a tracked question. The first request must target
# exactly one human. The message body IS the ask and must be
# decision-self-sufficient (why the question exists + recent-context recap +
# the single question and your recommendation); pass 2–4 --options (JSON) for
# a clean pick, or omit them for a free-text answer.
cat <<'EOF' | first-tree chat create --to alice --request \
--options '[{"label":"Ship","description":"Roll the migration now"},{"label":"Hold","description":"Wait 24h"}]'
## Why this question exists
Migration 0021 drops the legacy column — irreversible, so shipping is your call.
## Recent context
The 0021 cleanup you asked for last week is done; the PR is approved and CI is
green.
## The question
Ship the destructive migration now? I would ship — the column has had no reads
for 30 days.
EOF
# Inline — `chat send` wakes a participant (agent or human). A plain send to a
# human is informational only — readable, then safely ignorable; any question
# the next step depends on goes through `chat ask` (a send never carries a
# blocking question). The recipient must be a participant of FIRST_TREE_CHAT_ID.
first-tree chat send code-agent "ship the PR"
# Stdin (multiline, markdown, special chars)
echo "long body" | first-tree chat send code-agent -f markdown
# Rich / multi-line bodies: write to a file, then read it with --message-file
# (or `-F`). This is the most robust form — the body never passes through the
# shell, so backticks (`code`), quotes, apostrophes, and newlines are sent
# byte-for-byte. Inlining such a body lets the shell run backticks as command
# substitution and break on quotes, silently mangling the message.
first-tree chat send code-agent -f markdown --message-file reply.md
first-tree chat send code-agent -f markdown -F - < reply.md # `-` = stdin
# Inline bodies must carry REAL newlines. A one-line quoted body written with
# `\n` escapes — chat send code-agent "line1\n\n**title**" — is rejected
# BEFORE anything is sent (ESCAPED_NEWLINES, exit 2): shells do not expand
# `\n` inside quotes, so the literal backslash-n would be stored and the
# message would render as one long unformatted line. The error prints a
# copyable heredoc retry form on stderr; resend via stdin:
cat <<'EOF' | first-tree chat send code-agent -f markdown
first line
**second** line
EOF
# Stdin bodies are never checked — piping is also the escape hatch for
# intentionally sending literal `\n` text.
# Ask a human a tracked question (red-dot + blocks the chat for them until they
# answer). `chat ask` targets a single human; the message body IS the ask and
# must be decision-self-sufficient for a reader who remembers nothing of the
# chat: why the question exists + a recap of the recent interactions + the
# single question and your recommendation, written for a reader holding none
# of the context (unpack every shorthand; name options by their concrete
# consequence). Omit --options for a free-text answer, or pass 2–4 --options
# (JSON) for a clean pick; add --multi-select to allow more than one.
cat <<'EOF' | first-tree chat ask alice \
--options '[{"label":"Ship","description":"Roll it now"},{"label":"Hold","description":"Wait 24h"}]'
## Why this question exists
Migration 0021 drops the legacy column — irreversible, so shipping is your call.
## Recent context
You asked for the 0021 cleanup yesterday; the PR is approved and CI is green.
## The question
Ship the destructive migration now? I would ship — the column has had no reads
for 30 days.
EOF
# Free-text ask (no options) — the same three-section body, answered in free
# text. `-F` reads the body from a file (shell-safe — same rationale as
# `chat send -F`)
first-tree chat ask alice --message-file ask-body.md
# `chat ask` always opens a fresh top-level question — there is no threading
# (no --reply-to) and no resolve command. An agent can only ASK — it cannot mark a question
# answered or close it. The human resolves it by answering in the web UI; a moot
# question is simply left open (the human works open questions oldest-first), and
# re-asking opens a NEW, independent question.
# Pull a non-member into the current chat first, then send normally. Use this
# for same-task stage / role handoffs.
first-tree chat invite code-agent
first-tree chat send code-agent "now we can talk"
# Browse
first-tree chat list
first-tree chat history <chatId>
# Self-description: a short topic label + a work summary + status report,
# updated independently through `chat update` (topic and description each on
# their own). The description carries task background + plan + progress, renders
# as Markdown, and shows at the top of the chat's right sidebar; agents also read
# it via `chat list` to self-locate (see the agent briefing's "Chat Topic &
# Description"). Keep blockers / decisions OUT of it — raise `chat ask <human>`
# for those. Owner-gated: the chat's creator may update it, and when
# no agent owner is present (human-created chats — Web / GitHub-sourced — or the
# creator left) every worker agent counts as the owner; a non-owner agent in a
# chat whose agent creator is still present is refused with 403.
first-tree chat update --topic "review PR #916"
first-tree chat update --description "Reviewing PR #916. **Plan:** address review findings, re-verify. **Progress:** 2/3 findings fixed."
first-tree chat update --topic "ship plan" --description "Drafting; next: hand to QA."
first-tree chat update --clear-description
# A one-line --description whose newlines are written as literal `\n` is rejected
# before the write: shell quotes do not expand `\n`, so it would persist and
# render as one long line with visible `\n` tokens. For a multi-line description
# pass real newlines — either an ANSI-C $'...' string, or `--description -` to
# read it from stdin/heredoc:
cat <<'EOF' | first-tree chat update --description -
Reviewing PR #916.
**Plan:** address review findings, re-verify.
**Progress:** 2/3 findings fixed.
EOF
# `chat set-topic` still works as a deprecated alias.
# Interactive
first-tree chat open code-agentchat send / chat invite operate on the chat identified by
FIRST_TREE_CHAT_ID, which the runtime injects into the agent's session
environment. The recipient must be a participant of that chat; if not,
invite first.
chat create is different: it creates a new task chat and writes the first
message in one command. Use it to split genuinely new work into a fresh chat.
Use chat send for replies/status in the current chat, and chat invite when
you want to add a non-member to the current chat before sending there. A
same-task handoff, such as architect to developer or developer to reviewer,
stays in the current chat; invite the next agent and send the handoff there.
Task creation is intentionally not idempotent. There is no operation id, and
the CLI does not automatically retry a create request. If the command reports
an unknown result after a network/server failure, check chat list or the Web
UI before running it again; the chat may already exist.
If a non-human agent includes itself in chat create --to, the server records
the originating agent in metadata and uses that agent's manager human as the
effective sender so the first message can wake the agent normally.
Org document library (docloop) — publish markdown design docs for team
review, pull the structured comments reviewers leave, reply, resolve, and
track document status. Feature-flagged server-side
(FIRST_TREE_DOCS_ENABLED); commands report HTTP 404 while the flag is off.
Publishing is idempotent on slug: the first publish creates the document
(version 1), every later publish of the same slug appends the next version.
The caller's own identity signs every write — agents author under their own
agent name, humans under their member identity.
first-tree doc
├── publish <file> [--slug <slug>] [--title <t>] [--project <p>]
│ [--note <n>] [--status <s>] [--if-changed] # create or append a version
├── get <slug> [--version <n>] # read metadata + markdown content
├── list [--project <p>] [--status <s>] [--limit <n>] [--cursor <c>]
├── comments <slug> [--status open|resolved] [--version <n>]
│ [--watch [seconds]] # list; --watch streams new ones as JSON lines
├── comment <slug> <body> [--quote <exact> [--prefix <t>] [--suffix <t>]] [--version <n>]
├── reply <commentId> <body> # reply in a thread
├── resolve <commentId> [--reopen] # close (or reopen) a thread
├── status <slug> [--set draft|in_review|approved|archived] # show or move status
├── import <dir> [--project <p>] [--status <s>] [--dry-run] # bulk-publish a directory of .md files
└── export <dir> [--project <p>] [--status <s>] # dump library to <slug>.md files + manifest.json
first-tree doc publish design.md --slug chat-rename --project first-tree --status in_review
first-tree doc comments chat-rename --status open --json
first-tree doc reply <commentId> "Addressed in v2 — see §3"
first-tree doc resolve <commentId>
first-tree doc publish design.md --slug chat-rename --note "responds to review round 1"
first-tree doc status chat-rename --set approvedSlug defaults to the slugified filename; title defaults to the file's first
markdown heading (required on the first publish). Comment anchors are
TextQuoteSelector-style (exact / prefix / suffix) against the markdown
source, so an agent can locate every comment in the file it holds without
line-number conventions. Comments whose quote no longer exists in the latest
version come back with outdated: true (computed on read). import skips
NODE.md / README.md index files and is idempotent (re-runs only add
versions for changed content); export is the guaranteed way out — plain
markdown files plus a manifest.json of metadata.
GitHub entity attention for the current chat. follow wires an entity's
webhook event stream into the chat (one routing line, chat-scoped);
unfollow explicitly stops this chat from tracking the entity and severs
every line wired into the chat for that entity, however it was created.
Creating a PR or issue never follows it automatically — declare the
dependency explicitly, immediately after creation. Use
first-tree github follow --help / first-tree github unfollow --help
for the full flag surface and conflict handling.
first-tree github
├── follow <entity> [--chat <chatId>] [--rebind] # route the entity's events into the chat
├── unfollow <entity> [--chat <chatId>] # sever all of the chat's lines for the entity
└── following [--chat <chatId>] [--json] # list entities wired into the chat
# Inside an agent session the chat is inferred from FIRST_TREE_CHAT_ID
first-tree github follow https://github.com/acme/api/pull/42
first-tree github follow acme/api#42 # issue vs PR resolved automatically
first-tree github follow acme/api@3f2a91c # commit
first-tree github following
first-tree github unfollow acme/api#42<entity> accepts a full GitHub URL, owner/repo#N, or owner/repo@<sha>.
A 409 means the same (human, delegate) line already lives in another chat
— --rebind MOVES it here (a line is never duplicated). unfollow is
idempotent: removed: 0 is success, not an error. Requires the org's
GitHub App installation to cover the repo (422 otherwise).
first-tree org
└── bind-tree <url> # record this org's Context Tree repo URL
bind-tree records the team's Context Tree URL in
organization_settings(context_tree). Used by the onboarding flow's
"create new tree" path, where the agent calls back into the server
after scaffolding the tree.
The background service that holds the client WebSocket and runs every
configured agent on this machine. Installed automatically by first-tree login on supported desktop platforms: launchd on macOS, systemd --user
on Linux, and a per-user Task Scheduler logon task on Windows.
On Windows, Task Scheduler only owns per-user logon/start triggering. A hidden First Tree supervisor loop owns the daemon child process, exit-code restart policy, stop intent, runtime marker PID, and logs. This is not a Windows Service / WinSW install and does not run before the Windows user logs in.
first-tree daemon
├── start [--no-interactive] [--foreground]
├── stop
├── restart
├── status
├── doctor
├── probe [--no-upload] [--json]
├── install-codex [--spec <spec>] [--json]
└── install-claude [--spec <spec>] [--json]
| Subcommand | Purpose |
|---|---|
start |
Start the daemon and connect every configured agent to the server. Fail-closed: exits 1 with NO_CREDENTIALS if no credentials.json exists; run login first. --foreground runs in the current shell (for debugging); the default installs/starts the service. |
stop |
Stop the service (preserves auto-start; bring it back with start). |
restart |
Restart the service. |
status |
Local service state + server binding + auth health. Runs in well under a second. |
doctor |
Walk Node version, config, server reachability, WS, agent registrations, the installed service file, and the runtime providers — each step reported. The runtime-provider rows run the real launch-verified probe (a 1-turn model call for claude-code, a codex doctor handshake for codex), so doctor makes live provider calls; it is a deliberate diagnostic, not a hot path. |
probe |
Launch-probe the local runtime providers on demand and upload the result to the server (PATCH /clients/:id/capabilities). This is the manual refresh for a client's advertised capabilities after a provider is installed / logged in. Each probe really launches its provider. --no-upload runs a credentials-free local-only diagnostic (probe + print, no server auth needed). --json (or the global --json) emits the capability snapshot as the machine-readable { ok, data } envelope on stdout. |
install-codex |
Install the native Codex runtime engine on this machine (npm install -g @openai/codex). First Tree does not bundle the ~225MB native codex binary by default — the runtime resolves an external codex from PATH, known install locations, or the macOS ChatGPT/Codex desktop app — so this is the on-demand remediation when the codex capability probes as missing. Runs the same tracked-subprocess install path as self-update, then re-probes so the freshly installed binary is reflected. Purely local (no credentials). --spec <spec> picks an npm dist-tag or exact version (default latest); --json emits the post-install capability snapshot as the { ok, data } envelope. |
install-claude |
Install the native Claude Code runtime engine on this machine (npm install -g @anthropic-ai/claude-code). First Tree does not bundle the ~210MB native claude binary by default — the runtime resolves a system claude (env override / PATH / well-known install dirs) — so this is the on-demand remediation when the claude-code capability probes as missing. Runs the same tracked-subprocess install path as self-update, then re-probes so the freshly installed binary is reflected. Purely local (no credentials). --spec <spec> picks an npm dist-tag or exact version (default latest); --json emits the post-install capability snapshot as the { ok, data } envelope. |
Capability refresh timing. The daemon launch-probes runtime providers at
startup and re-probes automatically on every WebSocket reconnect. A full real
re-probe of all providers runs only when there is no prior snapshot or one is
older than 24h; otherwise each provider is re-validated individually — a
still-launchable, still-logged-in provider keeps its prior ok for free
(resolve + auth re-checked, no session smoke re-run), a provider that lost its
binary or login downgrades, and a non-ok provider is fully re-probed so it can
recover. So a machine missing an optional provider (e.g. no tmux for
claude-code-tui) does not re-smoke its healthy providers on every reconnect.
While the daemon stays connected, it also runs a bounded background
re-probe whenever any provider is not yet ok — so installing or logging into
a provider is noticed within a bounded time without a restart or reconnect. The
poll starts ~15s after the degraded state is seen and backs off to a 5-minute
ceiling, re-probes only the non-ok providers (already-ok ones stay on the
free cached path), uploads only when the snapshot actually changes, and stops
once every provider is ok. daemon probe remains the manual, on-demand path
to force an immediate full re-probe + upload.
The top-level first-tree status is the cross-subsystem overview that
calls daemon status internally and adds server/auth/agent rows.
Read and write this machine's client.yaml. The file lives at
~/.first-tree/config/client.yaml (or the staging/dev channel's
equivalent — see docs/development/local-dev-isolation.md).
first-tree config show # every key/value
first-tree config show server.url # dotted-key read
first-tree config show --show-secrets # un-mask sensitive fields
first-tree config set update.policy auto
first-tree config get update.policy # alias for `show <key>`Agent-side runtime configuration (model / prompt / MCP / env / repos) is
not here — it lives in first-tree agent config ... and mutates the
server-side agent_configs row through the Admin API.
Context Tree creation, structural validation, and hierarchy browsing. The
tree namespace carries verify, tree, and init; the rest (migrate /
upgrade / status / codeowners / claude-hook / inject / review /
automation / skill groups) was retired in the 2026-06 cleanup because the
cloud now owns workspace + tree provisioning and the client runtime inlines its
own skill payload install (see PR following #844).
first-tree tree
├── init [options] # create a new team Context Tree repo with local gh
├── verify [--tree-path PATH] # validate a Context Tree repo
└── tree [path] [-L depth] [-P pattern] # browse Context Tree nodes as a hierarchy
first-tree tree init creates a brand-new team Context Tree repository with the
user's local gh: it creates the repo (one path for user- and org-owned repos),
scaffolds a minimal valid tree (root NODE.md + members index + a creator member
node), self-verifies before pushing, pushes, and — unless --no-bind — binds the
org's context_tree setting and surfaces guidance for adding the repo to the
team's GitHub App installation. It does not seed .github/workflows/validate-tree.yml
by default (that needs the interactive workflow gh scope); pass --with-workflow
to include it. In the bound path the repo is created under the team's GitHub App
installation account (so the installation can cover it), and tree init refuses
to replace an existing team binding unless --rebind is passed. Key options:
--owner, --name, --title, --public, --dir, --with-workflow, --no-bind,
--rebind, --org. Run first-tree tree init --help for the full list.
Run first-tree tree verify --help for options.
first-tree tree tree [path] resolves path relative to the current
working directory, then renders from the current git repository root down
to that target directory and its descendants. Without path, the target is
the current directory. The target must be an existing directory inside the
current git repo.
Directory nodes come from that directory's NODE.md. Leaf nodes come from
Markdown files other than NODE.md, AGENTS.md, and CLAUDE.md. A
Markdown file is renderable only when its YAML frontmatter has a non-empty
string title and a non-empty array owners; description is optional.
The owners field is used for filtering and included in JSON, but is not
shown in human output. Hidden paths and common generated directories
(node_modules, __pycache__, dist, build, .next, .turbo) are
skipped.
Human output starts with the Context Tree git checkout branch, then the
rendered tree. When the current branch is not exactly main, master, or
origin/main, the branch line is followed by a stale-tree warning:
Detached HEAD checkouts whose commit matches refs/remotes/origin/main are
reported as origin/main.
Branch: feature/stale-tree
Warning: current branch "feature/stale-tree" is not main/master; it may be stale. Switch to main/master.
The rendered tree's node labels use:
relative/path/ [Title] -> Description
relative/path.md [Title]
Directory labels end with /. The repository root line uses the repo
directory name, for example context-tree/ [Context Tree] -> Root index for the First Tree context tree.. When description is missing,
the -> Description suffix is omitted.
Options:
-L, --level <depth>— maximum descendant depth below the target directory. Ancestors from the git repo root to the target are always kept. For path-tolerant CLI use,tree tree -L docs/developmentis treated astree tree docs/development;tree tree -L 2 docs/developmentapplies depth2to that path.-P, --pattern <pattern>— case-sensitive shell-style glob filter matched against relative path, filename,title, anddescription; matching descendants keep their ancestors visible.
With global --json or FIRST_TREE_JSON=1, first-tree tree tree
emits a single { ok: true, data } envelope on stdout. data.root is the
git repo root, data.target is the resolved target directory relative to
that root, and data.options records the parsed level, pattern, and
effective path. data.branch reports the current tree checkout as
{ name, isMainline, warning }; warning is null for main, master,
and origin/main, including detached HEAD checkouts that match
refs/remotes/origin/main; otherwise it contains the same stale-tree warning
string shown in human mode. data.tree contains the same filtered hierarchy as
structured nodes with kind, name, relativePath, depth, metadata,
hasNode, and children fields; metadata includes title, optional
description, and owners. Human tree text and branch warnings are not
written to stderr in JSON mode, so stdout stays reserved for machine-readable
JSON.
Most environment variables use the FIRST_TREE_ prefix.
| Variable | Purpose | Default |
|---|---|---|
FIRST_TREE_HOME |
Override the CLI home directory for config, data, and agent workspaces. | Channel-dependent: ~/.first-tree (prod), ~/.first-tree-staging (staging), ~/.first-tree-dev (dev). |
FIRST_TREE_SERVER_URL |
Server URL override for login <code> and fallback for other commands; otherwise login uses the CLI channel default. |
— |
FIRST_TREE_LOG_LEVEL |
Log level (trace / debug / info / warn / error / fatal). |
info |
FIRST_TREE_JSON |
JSON output mode (equivalent to --json). |
— |
A launchd / systemd / Task Scheduler daemon does not inherit your
interactive login-shell environment, so anything your shell exports (commonly
an HTTP_PROXY / HTTPS_PROXY for users behind a network proxy, or
FIRST_TREE_CLIENT_SENTRY_ENABLED=false for Client Sentry opt-out) is invisible
to the background daemon and the agent runtimes it spawns. That is why an
interactive claude / git can work while the daemon's calls to
api.anthropic.com / github.com fail.
To supply that environment, create daemon.env under your channel's
FIRST_TREE_HOME with simple KEY=VALUE lines. The path is channel-specific
(~/.first-tree/daemon.env on prod, ~/.first-tree-staging/daemon.env on
staging, ~/.first-tree-dev/daemon.env on dev) — it must match the channel of
the daemon that reads it:
HTTPS_PROXY=http://127.0.0.1:7897
HTTP_PROXY=http://127.0.0.1:7897
NO_PROXY=localhost,127.0.0.1
FIRST_TREE_CLIENT_SENTRY_ENABLED=falseThe daemon loads this file on start and passes the values to every child it
spawns. First Tree is compatible with your proxy — it only ever reads
this file and never writes your proxy into it on your behalf. Values already
present in the daemon's environment are preserved (the file fills gaps, it does
not override). Edit or delete the file freely, then restart the daemon
(<channel> daemon stop && <channel> daemon start) to apply changes. See
troubleshooting/proxy.md.
These are mentioned for completeness; operators don't set them in shell rc.
| Variable | Purpose |
|---|---|
FIRST_TREE_SERVICE_MODE |
Supervisor → child flag baked into launchd/systemd templates and set by the Windows supervisor loop. |
These are client-side update behavior tunables. They do not select a release
channel; channel identity comes from the installed package / binary
(first-tree, first-tree-staging, or first-tree-dev).
| Variable | Purpose |
|---|---|
FIRST_TREE_UPDATE_RESTART_CHECK_INTERVAL_SECONDS |
Frequency of the upgrade-restart watchdog. |
FIRST_TREE_UPDATE_RESTART_QUIET_SECONDS |
Quiet window the upgrade flow waits for before restarting. |
FIRST_TREE_UPDATE_PROMPT_TIMEOUT_SECONDS |
Interactive upgrade prompt timeout. |
FIRST_TREE_UPDATE_POLICY |
auto / prompt / off. Persisted via first-tree config set update.policy .... |
Per-agent bearer tokens are gone — every agent on a signed-in machine authenticates as the signed-in member. The runtime injects these so an agent process can talk to the server without extra setup:
| Variable | Purpose |
|---|---|
FIRST_TREE_ACCESS_TOKEN |
The signed-in member's access JWT (short-lived). |
FIRST_TREE_AGENT_ID |
The agent's own UUID — the CLI uses it to identify the sender. |
FIRST_TREE_CLIENT_ID |
The client (machine) the agent is bound to. |
FIRST_TREE_CHAT_ID |
The chat the current session is bound to. Used by chat send / chat invite. |
FIRST_TREE_SERVER_URL |
Server URL override; falls back to client config. |
These configure the SaaS server image (packages/server/dist/index.mjs)
and are not used by the CLI. They are listed here for ops reference.
Identity / channel:
| Variable | Purpose | Default |
|---|---|---|
FIRST_TREE_CHANNEL |
Deployment channel (prod / staging / dev). |
dev |
FIRST_TREE_DATABASE_URL |
PostgreSQL connection URL. | — (required) |
FIRST_TREE_PORT |
HTTP listen port. | 8000 |
FIRST_TREE_HOST |
Bind address. | 127.0.0.1 |
FIRST_TREE_PUBLIC_URL |
Public-facing server URL. Used to stamp the issuer on short connect codes and to build invite-link URLs + the GitHub OAuth callback. Required in production. | — |
FIRST_TREE_PORTABLE_DOWNLOAD_BASE_URL |
Base URL for the prod/staging portable installer and artifact mirror. Do not include a channel suffix; the server appends the channel's publicInstallerPath (for example, prod/install.sh). |
https://download.first-tree.ai/releases |
FIRST_TREE_CORS_ORIGIN |
Allowed origin for the web console. | — |
FIRST_TREE_TRUST_PROXY |
Trust the reverse-proxy X-Forwarded-* headers. |
false |
FIRST_TREE_WORKSPACES_ROOT |
Where agent worktrees are materialised on the host. | derived from FIRST_TREE_HOME |
Command update advertisement:
There is no FIRST_TREE_UPDATE_CHANNEL. Published channels have separate npm
package identities, and the server polls the package selected by
FIRST_TREE_CHANNEL.
| Variable | Purpose | Default |
|---|---|---|
FIRST_TREE_COMMAND_VERSION |
Bootstrap CLI version advertised before the npm-registry poller succeeds. The Docker image stamps this from the Command package version at build time. | image build arg |
FIRST_TREE_UPDATE_POLL_INTERVAL_MINUTES |
How often the server polls npm for the selected channel package's latest version. |
60 |
FIRST_TREE_UPDATE_REGISTRY_URL |
npm registry override for the server-side update-version poller. | https://registry.npmjs.org |
Secrets:
| Variable | Purpose | Production |
|---|---|---|
FIRST_TREE_JWT_SECRET |
JWT signing key. channel=dev local development auto-generates a value when omitted. |
Required for staging/prod |
FIRST_TREE_ENCRYPTION_KEY |
AES-256-GCM key for encrypted server-side secrets (GitHub tokens, org-settings secrets). Must be 32 bytes encoded as 64-char hex or base64url. channel=dev local development auto-generates a value when omitted. |
Required for staging/prod |
The server Docker image sets NODE_ENV=production, which disables generated
server secrets even if FIRST_TREE_CHANNEL is omitted or defaults to dev.
Auth lifetimes:
| Variable | Default |
|---|---|
FIRST_TREE_AUTH_ACCESS_TOKEN_EXPIRY |
30m |
FIRST_TREE_AUTH_REFRESH_TOKEN_EXPIRY |
30d |
FIRST_TREE_AUTH_CONNECT_TOKEN_EXPIRY |
10m |
GitHub App / OAuth:
| Variable | Purpose |
|---|---|
FIRST_TREE_GITHUB_APP_ID |
GitHub App numeric id. |
FIRST_TREE_GITHUB_APP_CLIENT_ID |
GitHub App OAuth client id. |
FIRST_TREE_GITHUB_APP_CLIENT_SECRET |
GitHub App OAuth client secret. |
FIRST_TREE_GITHUB_APP_PRIVATE_KEY |
GitHub App signing key (PEM body). |
FIRST_TREE_GITHUB_APP_WEBHOOK_SECRET |
Webhook HMAC secret. |
FIRST_TREE_GITHUB_APP_SLUG |
Optional explicit slug override. |
Rate limits:
| Variable | Default |
|---|---|
FIRST_TREE_RATE_LIMIT_MAX |
3000 |
The server applies this as one actor-aware global safety cap per minute. It keys by agent id, then user id, then request IP for unauthenticated traffic. Old per-route rate-limit env vars are no longer read.
Inbox / WS / archive sweeper:
| Variable | Default |
|---|---|
FIRST_TREE_INBOX_MAX_IN_FLIGHT_PER_AGENT |
server-tuned |
FIRST_TREE_WS_MAX_PAYLOAD |
262144 (256 KiB) |
FIRST_TREE_ARCHIVE_SWEEP_INTERVAL_SECONDS |
300 (set 0 to disable) |
FIRST_TREE_ARCHIVE_MAPPED_IDLE_SECONDS |
3600 |
FIRST_TREE_ARCHIVE_MAPPED_IDLE_SECONDS is the GitHub-source archive idle
threshold. Mapped chats also require all bound entities to be closed/merged;
source=github chats with no mapping use the same idle threshold.
Observability:
| Variable | Purpose | Default |
|---|---|---|
FIRST_TREE_LOG_LEVEL |
Server log level. | info |
FIRST_TREE_OTEL_ENDPOINT |
OTLP/HTTP traces endpoint. Non-empty enables tracing. | "" |
FIRST_TREE_OTEL_HEADERS |
OTLP headers as key1=val1,key2=val2. Typically holds the write token. |
"" |
FIRST_TREE_OTEL_ENVIRONMENT |
Deployment label emitted as deployment.environment.name. |
development |
FIRST_TREE_OTEL_CAPTURE_CLIENT_IP |
Capture client IP attribute on traces. | false |
VITE_SENTRY_DSN |
Public browser DSN for Web Console errors in the first-tree-web Sentry project. |
unset |
VITE_SENTRY_ENABLED |
Explicit Web Sentry switch; false / 0 / off disables even when a DSN is present. |
enabled when DSN exists |
VITE_SENTRY_ENVIRONMENT |
Web Sentry environment label. | host/mode-derived |
VITE_SENTRY_TRACES_SAMPLE_RATE |
Web Sentry trace sample rate (0.0–1.0). |
0.1 |
FIRST_TREE_CLIENT_SENTRY_DSN |
Client daemon/runtime DSN for the first-tree-client Sentry project. |
unset |
FIRST_TREE_CLIENT_SENTRY_ENABLED |
Explicit Client Sentry operator switch; false / 0 / off disables even when a DSN is present. |
enabled when DSN exists |
FIRST_TREE_CLIENT_SENTRY_ENVIRONMENT |
Client Sentry environment label. | NODE_ENV or development |
FIRST_TREE_CLIENT_SENTRY_TRACES_SAMPLE_RATE |
Client Sentry trace sample rate (0.0–1.0). |
0.05 |
FIRST_TREE_GIT_SHA |
Git SHA stamped onto Web/Client Sentry releases and tags when provided by CI. | unknown |
See observability.md for the full config reference, backend cheat sheet, and troubleshooting recipes.
~/.first-tree/ # FIRST_TREE_HOME default for the prod channel
├── config/
│ ├── client.yaml # this machine's client config (server.url, client.id)
│ ├── credentials.json # access + refresh JWT (mode 0600)
│ └── agents/
│ └── <name>/
│ └── agent.yaml # agentId + runtime
├── data/
│ ├── context-tree-repos/ # legacy shared Context Tree pool (retained for old installs; new clones live per-agent)
│ ├── sessions/ # per-agent session registry
│ └── workspaces/
│ └── <agent-name>/ # per-agent home (cwd is shared across chats)
│ ├── context-tree/ # agent-managed Context Tree clone (agent clones/pulls it per its briefing)
│ └── worktrees/ # per-task worktrees the agent creates and cleans up
└── logs/ # daemon stderr / stdout (macOS)
When FIRST_TREE_HOME is set, replace ~/.first-tree/ with that location. Staging and dev channels use ~/.first-tree-staging/ and ~/.first-tree-dev/ respectively as their channel-default home.
Priority from high to low:
- CLI arguments
- Environment variables (
FIRST_TREE_*) - Config files (
~/.first-tree/config/client.yaml, or the staging/dev channel's equivalent) - Built-in defaults
After first-tree upgrade or after running logout + login <code> on a deployment-bump cycle:
first-tree status # CLI version + service + server + auth + agents
first-tree daemon doctor # service + agent configs + WS reachability
first-tree --help # top-level verbs + namespaces