The fastagent CLI is the file-first workflow for creating, inspecting, serving, and operating an agent.
fastagent <command> [args] [options]Most commands take an optional workspace directory (the agent is there, or in its ./fastagent/). When omitted, the current directory is used.
Commands
| Command | Purpose |
|---|---|
init [dir] |
Scaffold a runnable agent. |
info [dir] |
Inspect what an agent assembles into without serving. |
models [search] |
List model specs. |
login [provider] |
Store provider credentials in the project-level <agent dir>/.secrets/auth.json (override: --auth-path / FASTAGENT_AUTH_PATH, dir: FASTAGENT_SECRETS_DIR). |
dev [dir] |
Serve locally with watch/reload. |
chat [dir] |
Open the same assembled agent in pi’s interactive TUI. |
invoke <message> [dir] |
Run one agent turn and exit. |
fire <name> [dir] |
Run one schedule’s turn immediately (authoring loop). |
schedule history <name> [dir] |
Print the run audit for a schedule (or wake). |
schedule list [dir] [--json] |
Everything that will fire: static schedules (next instant) + pending wake-ups. |
schedule cancel <id> [dir] |
Remove a pending wake-up (operator kill switch). |
tool <name> <json> [dir] |
Run one discovered tool directly. |
| `add github | telegram |
add skill <source> [dir] |
Vendor an Agent Skills skill into skills/. |
deploy docker [dir] |
Generate fastagent.compose.yml + the portable Dockerfile/.dockerignore for local Docker: one agent service, loopback port, /data state volume, and exact env-var names. --tunnel --run starts app+tunnel, reads the ephemeral URL, and auto-registers Telegram, locally onboarded Slack, and Feishu/Lark webhooks. Existing files stay authoritative unless --force; durable ingress/proxy/DNS/TLS remain operator-owned. |
deploy fly [dir] |
Generate Fly.io artifacts (fly.toml/Dockerfile/.dockerignore, autostop=suspend, state→volume) and print a flyctl runbook + webhook step. --run drives flyctl to completion (idempotent, resumable; carries your local credential; needs flyctl). --stop (stop instead of suspend), --no-scale-to-zero (keep one machine up), --force (overwrite artifacts). |
deploy railway [dir] |
Generate Railway artifacts (railway.json with healthcheckPath=/health, plus the shared Dockerfile/.dockerignore) and print a railway runbook: init a project, create a service (railway add --service), attach a /data volume, set the state root + secrets as variables (railway variables set, before the first deploy), railway up, then mint a domain (railway domain) and register the webhook. Scale-to-zero (App Sleeping) is a dashboard-only step the runbook states. --run drives the railway CLI to completion on an UNLINKED dir (auth → init/add/volume → variables → railway up → mint domain → telegram webhook; carries your local credential; needs the railway CLI); a dir already linked to a project is refused unless --into-linked (provision into it) — a routine redeploy is just railway up. --force overwrites artifacts. |
deploy agentcore [dir] |
Generate AWS Bedrock AgentCore artifacts (agentcore.template.yaml — one CloudFormation stack: Runtime + forwarder Lambda/Function URL for webhooks + EventBridge rules for schedules and wake alarms — plus lambda/forwarder.js and the shared Dockerfile/.dockerignore) and print an aws/docker runbook. State rides platform SessionStorage at /mnt/state (tied to the Runtime resource). --run drives it end to end: identity/region/docker gates, ECR, local docker buildx arm64 push, cloudformation deploy (secrets via a 0600 params file), stack outputs, webhook registration, then stops the ingress session so the new image serves immediately. Long-connection channels are unsupported (switch to webhook mode); a kept generated template that no longer matches the definition gates --run until --force. |
logs agentcore [dir] |
Discover the deployed stack’s CloudWatch log group and run aws logs tail. Defaults to the Runtime’s application stdout/stderr; --source forwarder selects the separate Lambda ingress logs. --since <duration> sets the history window and --follow keeps polling. Read-only; does not change FASTAGENT_LOG_LEVEL. |
start [dir] |
Serve without watch. |
fastagent init
fastagent init [dir] [--minimal] [--no-install] [--agent-dir <name>|--flat]Creates a self-iterating agent — it can edit its own definition (persona.md and skills are re-read every turn). A fresh agent has persona.md (the agent’s identity: how to improve yourself), a writing-great-skills example skill (from mattpocock/skills — the guide to authoring skills), a fetch-url example code tool, config, .secrets/.env.example, and .gitignore. No AGENTS.md is scaffolded (it is project context, not identity); an existing one is kept untouched. Everything is written offline; by default it also writes package.json and runs npm install. Ignore hygiene is two scaffolded files: the agent .gitignore (node_modules/, .state/, a stray .env) and .secrets/.gitignore (everything but .env.example). fastagent writes both once, at init — no later command reads, rewrites or verifies them, so they are yours. They are separate on purpose: the root one is the file you will edit, and a nested .gitignore outranks it, so the credentials stay protected either way.
Where the files land — no detection and no prompt. By default the WHOLE agent — definition, config, .secrets/, .state/ — goes into ./fastagent/; the directory around it gets zero writes and becomes the agent’s WORKSPACE when you point fastagent there. The agent self-contains its package.json, so the workspace’s manifest and lockfile are never touched. init refuses when the target already holds a fastagent.config.* (already an agent) or any other content.
--agent-dir <name> names that directory anything you like: the name never decides what IS an agent — a fastagent.config.* does, so ./bot/ and ./fastagent/ are equally agents. The name carries weight in exactly one place, and only among agents already identified: when a workspace holds several and nothing selects, the one named fastagent answers (see FASTAGENT_AGENT below). It must be a single directory name (a path would put the agent where fastagent’s one-level lookup could not find it).
--agent-dir . (spelled --flat) puts the identical shape in the directory itself — for a standalone agent repo or a monorepo package, where the agent’s tools operate on its own definition. Adopting a directory is the point, so every file that already exists is kept (reported, never overwritten); only a fastagent.config.* refuses, because that means it is already an agent.
What a served agent’s workspace turns out to be is not decided here: it is whatever directory you later point fastagent at (see dev/start below). init’s one placement duty is to refuse a target the lookup could never SELECT — an agent already resolving AT dir, which wins over anything inside it and would hide the new one. A second agent BESIDE an existing one is fine and supported: several agents can share one workspace (an engineer’s, a PM’s, a content owner’s, all driving the same repository), and FASTAGENT_AGENT=<name> picks between them — the one named fastagent answers by default. init prints the note when a workspace crosses into that shape. The config is a DECLARATION, not configuration: its contents may be export default {} (a model can come from --model), but a directory has to SAY it is an agent — the job package.json and Cargo.toml do for their tools. A directory holding nothing else is already a complete agent.
Options:
| Option | Meaning |
|---|---|
--minimal |
persona.md + the example skill + config only — no code tool, package.json, or install. |
--no-install |
Scaffold everything but skip npm install. |
--agent-dir <name> |
Name the agent directory (default fastagent; . = dir itself). |
--flat |
Alias for --agent-dir . — the directory IS the agent; existing files are kept. |
fastagent info
fastagent info [dir] [--json] [--model provider/modelId] [--auth-path file] [--sessions-dir dir]Prints the assembled surface without starting a server:
- model source,
- config path,
- persona presence and context files (
AGENTS.md), - skills and their diagnostics,
- the complete coding-tool set (
read/grep/find/ls/bash/edit/write), plus authored tools and collisions, - channel files,
- schedules (name + next fire instant; a broken schedule file is reported here, not first at
dev), - whether self-scheduling (
selfSchedule) is on, - session directory.
info is read-only: it does not create sessions or modify .state//.secrets/.
fastagent models
fastagent models [search]Lists available model specs in provider/modelId form. Pass a search string to filter.
Use a listed spec with --model, FASTAGENT_MODEL, or fastagent.config.*.
fastagent login
fastagent login [provider] [--auth-path file] [--no-input]Authenticates a model provider and stores credentials in the project-level <agent dir>/.secrets/auth.json (dir override: FASTAGENT_SECRETS_DIR; file override: --auth-path / FASTAGENT_AUTH_PATH; run it OUTSIDE any agent — no ./fastagent/ in the current directory — to write the global ~/.fastagent/.secrets/auth.json, which it announces on stderr). Inside an agent but not at its root (fastagent/tools/), it refuses and tells you where to cd. There is no implicit fallback between the project and global files — the default is project-level for isolation (different agents can use different accounts) and fail-visibly (a missing credential surfaces instead of being masked by a machine-global one absent on a fresh box). So cd into your agent before logging in. FastAgent uses its own credential file, separate from pi’s CLI state.
An API-key login is verified immediately with one minimal request (OAuth needs no check — completing the flow proves the credential): a definitive rejection (HTTP 401) removes the bad key and prompts for it again on the spot (cancel to stop), so a mistyped key is corrected at login time instead of failing at the first invoke; an inconclusive failure (network, quota, permissions) keeps the key and prints the provider’s message.
The directory holding auth.json is managed as a secrets directory, whichever knob named it:
every credential write (including an OAuth refresh mid-run) re-applies 0700 to it, and where the
process cannot chmod it the write fails instead of leaving the credential readable. Point
--auth-path / FASTAGENT_AUTH_PATH inside a directory this process owns, not a shared one others
need to read.
Running several agents off one account on your dev machine? Point them all at the one global file: set FASTAGENT_AUTH_PATH=~/.fastagent/.secrets/auth.json (a .env entry, a shell env var, or --auth-path), or just login from outside any agent. A leading ~ is expanded to your home dir in --auth-path and FASTAGENT_AUTH_PATH (shell variables like $HOME are not — use ~ or an absolute path). Sharing one file is safe — a single cross-process lock serializes OAuth refresh, so concurrent instances always read the latest token. (What is not safe is copying the file around: two files over one grant each rotate the single-use refresh token and break the other.)
fastagent dev
fastagent dev [dir] [--port N] [--bind addr] [--model provider/modelId] [--auth-path file] [--no-watch] [--tunnel] [--no-input]Assembles the agent and serves it locally. persona.md/AGENTS.md/skills/ are re-read every turn (edits go
live next turn, no restart); a supervisor restarts the worker on edits to the code inputs —
tools/, channels/, schedules/, fastagent.config.*, package.json, .secrets/.env.
With no model set and a terminal attached, dev first shows the full model catalog — models whose
provider already has credentials are listed first and annotated with the source (e.g. ready — OPENAI_API_KEY); picking one that needs auth runs the login flow inline — then writes the choice
back to the config (same for start / invoke / fire / chat / deploy). Pass --model or set
FASTAGENT_MODEL to skip the prompt.
Options:
| Option | Meaning |
|---|---|
--port N |
Override http.port / default 8787. |
--model spec |
Override model selection. |
--auth-path file |
Override the credential file (default <agent dir>/.secrets/auth.json). |
--no-watch |
Serve once without the watch supervisor. |
--tunnel |
Open a Cloudflare quick tunnel for webhook testing. |
--no-input |
Never prompt (CI/scripts) — e.g. the first-run model pick becomes an actionable error instead of a question. |
Model precedence:
--model > FASTAGENT_MODEL > fastagent.config.* modelfastagent attach
fastagent attach <session> [dir] [--url url --token token]Attach to a session served by a running dev/start with sessionControl: true in the config:
stream its live events (text, tool activity, run boundaries), steer the active run by typing a
line — or, with no run active, start one (the line becomes a new prompt over the remote data
plane) — /abort to stop a run, /commands to list the names the agent defines (skills; name one in
a normal message — this composer does not expand /name), Ctrl+C to detach. A run YOU started from attach is driven by
attach’s own connection, so detaching cancels it (channel-started runs are unaffected). Discovers the local endpoint from
<stateRoot>/control.json; --url/--token reach a remote serve. Speaks the same wire protocol a
Web panel or desktop app uses (connectSessionControl).
fastagent chat
fastagent chat [dir] [--model provider/modelId] [--auth-path file]Opens the same assembled agent in pi’s interactive TUI. This is useful for trying the agent before serving it through channels.
Auth is fastagent’s, same as every other command: --auth-path > FASTAGENT_AUTH_PATH > the
agent auth.json. Log in with fastagent login (or pi’s /login inside the TUI, which writes
to the same file). With no model set, chat runs the same first-run picker as the serving commands
(credential-annotated catalog, inline login) and writes the choice back to the config.
fastagent invoke
fastagent invoke <message> [dir] [--model provider/modelId] [--auth-path file] [--no-input]Runs one turn through the same agent assembly and exits:
- answer text streams to stdout,
- tool and diagnostic lines go to stderr,
- a
failedterminal event exits non-zero.
Use this for smoke tests and scripts.
fastagent fire
fastagent fire <name> [dir] [--model provider/modelId] [--auth-path file] [--no-input]Runs ONE schedule’s turn immediately — the authoring loop for schedules (like invoke is for a
prompt). Fires schedules/<name>.ts now, without waiting for its cron, using the schedule’s stable
session, so you see exactly what the served scheduler would do:
- answer text streams to stdout, tool/diagnostic lines to stderr, a
failedturn exits non-zero (likeinvoke), - no name → usage on stderr, exit 2; an unknown schedule name → exit 1 with the available names,
- it does not advance the schedule’s fire state — a test run never makes the running scheduler skip the real next run.
A schedules/<name>.ts file default-exports defineSchedule({ cron, tz?, prompt }); the scheduler
fires the agent on that cron when you dev/start. Output is the agent’s tools’ job — the scheduler
only fires and logs. See the API reference.
fastagent schedule history
fastagent schedule history <name> [dir] [--json]Prints the run audit for one schedule — or wake for the agent’s self-scheduled wake-ups: when each run
fired, its outcome (completed / failed / deferred), duration, and a preview of the reply or error.
The answer to “did last night’s run silently fail?”. Read-only (reads <state root>/schedule/runs.jsonl,
written by the serving scheduler); --json prints the full records, including the complete reply text.
fastagent schedule list [dir] shows everything that will fire, from BOTH producers: the static
schedules/ files (name + next cron instant) and the agent’s pending self-scheduled wake-ups (id, next
fire, one-shot/cron, session, prompt), with --json for the machine-readable form;
fastagent schedule cancel <id> [dir] removes one wake-up: the
operator’s kill switch for a runaway recurring wake (the agent’s own is the unwake tool, which is
session-scoped).
fastagent tool
fastagent tool <name> '<json-args>' [dir]Runs one discovered or configured tool directly, without a model or server. The call receives its workspace cwd but no session manager; a session-dependent tool reports that requirement itself.
Example:
fastagent tool fetch-url '{"url":"https://example.com"}'fastagent add github|telegram|slack|feishu|lark
fastagent add github [dir]
fastagent add telegram [dir]
fastagent add slack [dir] # create/install an internal app; --no-onboard scaffolds only
fastagent add feishu [dir] # 飞书 (open.feishu.cn) — also CREATES the app (scan-to-create; credentials → .secrets/.env)
fastagent add lark [dir] # Lark intl — opens console + collects/validates credentialsCreates a channels/<kind>.ts file with adapter glue and appends env placeholders to .secrets/.env.example when possible. The channel’s GENERATED secrets (telegram’s TELEGRAM_SECRET_TOKEN, github’s GITHUB_WEBHOOK_SECRET — random strings the user contributes nothing to) are written to .secrets/.env, leaving only genuinely-manual values (e.g. TELEGRAM_BOT_TOKEN from BotFather) as next steps — they are covered by the .secrets/.gitignore written at init. Everything (glue, companion tool, secrets) lands in the agent dir (./fastagent/) — the same place dev/start discover channels. The channel file is written once and is yours after that; a companion tool (tools/slack-send.ts, tools/telegram-send.ts, …) is the package’s and is rewritten on every add, so after upgrading the package, re-run add <kind> (--no-onboard skips the app prompts) to refresh it.
An enabled channels/*.ts|*.js|*.mjs file must load successfully or dev / start fails. To
intentionally disable one, rename it to e.g. channels/telegram.ts.disabled; channel files, not config,
are the enable/disable source of truth.
Slack scaffolds channels/slack.ts plus tools/slack-send.ts; --group-behavior context|mentions
selects the created app’s manifest scopes/events (the runtime has one behavior; it hears what the app
is allowed to), defaulting to context-aware context; choose mentions explicitly for least privilege. By default it opens Slack’s App Configuration Token page,
creates a new internal app with agent_view, native Agent streaming, and suggested prompts through
apps.manifest.create, installs it
through OAuth, and writes the Bot User OAuth Token + the Signing Secret to .secrets/.env. The configuration refresh token stays owner-readable
under <state root>/channels/slack/ and is used locally by dev --tunnel / deploy --run to update the
Events API URL; it never travels to the host. --no-onboard preserves the manual scaffold-only path.
--replace-config skips the menu and directly replaces the local App Configuration token pair — the
repair when automatic Request URL updates fail because the tokens expired or were revoked. It works only
on the machine that onboarded the app (the pair lives in its local state); other machines set the Request
URL manually in the Slack console.
See:
fastagent add skill
fastagent add skill <source> [dir] [--update]Vendors an Agent Skills skill into skills/<name>/ in the agent dir (./fastagent/skills/ in the default placement). Sources can be:
- a GitHub-style ref,
- a local path,
- a bare name from local global skill directories.
Use --update to overwrite an existing vendored skill. Review the result with git diff before deploying.
fastagent start
fastagent start [dir] [--port N] [--bind addr] [--model provider/modelId] [--sessions-dir dir] [--auth-path file] [--tunnel] [--no-input]Runs the agent in production posture: no watch, same assembly as dev.
Port precedence:
--port > PORT > fastagent.config.* http.port > 8787Bind precedence (same for dev):
--bind > fastagent.config.* http.host > all interfacesSession directory precedence:
FASTAGENT_STATE_DIR > <agent dir>/.state (mutable machine state)
FASTAGENT_SECRETS_DIR > <agent dir>/.secrets (.env + auth.json)
--sessions-dir > FASTAGENT_SESSIONS_DIR > <state root>/sessionsFASTAGENT_AGENT — which agent, when a workspace holds several. Not a path knob: it is an input to
every command, because several agents can share one workspace (an engineer’s, a PM’s and a content
owner’s agent all driving the same repository). It ASSERTS the agent by directory name — a directory
without one by that name refuses rather than serving a different agent — and with nothing set, the
agent named fastagent (the init default) answers. Scope it per repository (an .envrc) or per
command (FASTAGENT_AGENT=pm fastagent dev); deploy bakes the selected agent into the image, so the
container never re-picks. Selection lives in your environment on purpose: it is per-person, and a file
committed to the shared workspace could not express “mine”.
For deployments, point the state root (sessions, channel state) and the secrets dir (the
seeded/rotated auth.json) at durable storage:
FASTAGENT_STATE_DIR=/data/.state FASTAGENT_SECRETS_DIR=/data/.secrets fastagent startGlobal options
Flags belong to their command and come after it: fastagent info --json, not
fastagent --json info. (Earlier releases accepted flags anywhere on the line; that form now fails
with unknown option, exit 2 — move the flag after the command/subcommand.)
Only two options are global:
| Option | Meaning |
|---|---|
-h, --help |
Print help. Works per command too: fastagent deploy --help, fastagent help deploy. |
-v, --version |
Print the package version. |
Recurring per-command options (same meaning everywhere they appear):
| Option | Commands | Meaning |
|---|---|---|
--bind <addr> |
dev, start |
Bind address — an IP literal, or localhost (read as 127.0.0.1). Default: all interfaces (containers need it); 127.0.0.1 keeps the port, /control/* included, off the LAN. Prefer this flag over http.host for a local-only bind — config travels into a deployed image, where deploy gates it. See Bind address. |
--no-input |
dev, start, invoke, fire, login, deploy |
Never prompt; missing information becomes an error with the flag to pass (deploy plan mode only warns on a missing model — --run gates). |
--model <provider/modelId> |
assembly commands | Model override (--model > FASTAGENT_MODEL > config). |
--auth-path <file> |
assembly commands, login |
Credentials file override. |
--json |
info, schedule history, schedule list |
Machine-readable output. |
Exit codes
| Code | Meaning |
|---|---|
0 |
Success (including help/version displays). |
1 |
Runtime failure — a failed turn, a broken definition, a deploy gate, an unknown tool/schedule name, invalid runtime configuration (e.g. a bad PORT env). |
2 |
Usage error — unknown command/flag, missing/empty/invalid arguments, conflicting flags. A mistyped command suggests the nearest one (fastagent depoly → “Did you mean deploy?”). |

