fongflow CLI
fongflow is FongFlow's command-line interface. It is command-only: no
interactive screens. It does three things:
runner: makes this computer a runner for Flow's "Run on my computer" step. This is its main job.flow: runs workflows and watches their runs from a shell script or CI.fofo: asks FoFo, the FongFlow assistant, from the terminal.capture: drops a note, piped text or a file into your Idea Capture inbox.todo: quick capture of todos.
ff is installed as a short alias: ff flow ls is the same as
fongflow flow ls.
How login works
The CLI uses the standard OAuth 2.0 Authorization Code flow with PKCE (RFC 7636 / RFC 8252 — "OAuth for native apps"):
fongflow loginstarts a tiny local web server on a loopback port (127.0.0.1) and opens your browser to the backend's/oauth/authorizeendpoint.- You log in on the web as usual. The authorization server redirects back to
the CLI's local
…/callbackURL with a one-time authorizationcode. - The CLI exchanges that code (plus the PKCE verifier) at
/oauth/tokenfor an access token and refresh token, then stores them locally. - Every subsequent command sends the access token as
Authorization: Bearer <token>. Tokens are refreshed automatically when they expire.
The public OAuth client (client_id = fongflow-cli) and its loopback redirect
URIs are seeded by the backend on startup
(backend/src/modules/oauth/bootstrap.ts).
Install / run
From the repo root:
pnpm install
pnpm --filter @fongflow/cli build
Then run it directly, or link it onto your PATH:
node cli/dist/index.js --help
# or, to get a global `fongflow` command:
cd cli && npm link
fongflow --help
During development you can skip the build step:
pnpm --filter @fongflow/cli dev -- --help
Configuration
| Setting | How to set it | Default |
|---|---|---|
| Backend URL | FONGFLOW_API_URL env var, or fongflow login --api <url> | https://fongflow.com |
| OAuth client id | FONGFLOW_CLIENT_ID env var | fongflow-cli |
| Config / token storage | FONGFLOW_HOME, else $XDG_CONFIG_HOME/fongflow | ~/.config/fongflow |
Credentials are written to ~/.config/fongflow/credentials.json with 0600
permissions.
Commands
Every list command takes --json. A command that fails exits non-zero.
fongflow login [--api <url>] [--no-browser] [--scope <scope>]
fongflow logout
fongflow whoami [--json]
# Runner (see below)
fongflow runner register | start | status | config | test | jobs | logs
fongflow runner install-service | stop | restart | uninstall-service | unregister | build-image
# Computer — a browser FongFlow AI uses for you (see below)
fongflow computer start [--browser auto|container|chrome] [--headed] [--image <img>] [--port <n>] [--chrome <path>]
# Flow — a workflow is a name (exact or a unique part of one) or an id
fongflow flow ls [--status active,paused] [--json]
fongflow flow run <workflow> [-i <json|@file|->] [--var k=v]… [--draft] [--no-wait] [-t <seconds>] [--json]
fongflow flow runs [workflow] [--status <s>] [-l <n>] [--json]
fongflow flow show <runId> [--json]
fongflow flow answer <runId> [field=value…] # no values: print the question
fongflow flow cancel <runId>
fongflow flow on|off <workflow>
# FoFo — one question per command; the answer streams to stdout
fongflow fofo <message...> [--new] [-c <conversationId>] [-a <agent>] [--json]
fongflow fofo ls | history [id] | agents
# Idea Capture (alias: idea) — words, piped stdin, or a file (≤ 50 MB)
fongflow capture [text...] [-t <title>] [-f <file>] [--json]
# ff capture "an idea" → the text
# git diff | ff capture → the diff
# git diff | ff capture "why" → titled "why", the words then the diff
# Todos
fongflow todo ls [--all] [-l <n>] [--json]
fongflow todo add <title...> [-p <priority>] [-d <date>] [--json]
fongflow todo done <id>
flow run waits until the run ends and prints what its Result steps
returned. Its exit code says how the run ended:
| Code | Meaning |
|---|---|
| 0 | succeeded |
| 1 | failed or cancelled |
| 2 | waiting on a question (flow answer resumes it) |
| 3 | still running when --timeout ran out |
A run waiting on a runner is not "waiting on a question": flow run keeps
waiting until the runner finishes the job. flow run runs the workflow's
active version; add --draft to run the latest saved draft.
fofo keeps its own conversations, separate from the web and dock chats.
Each question continues the last one; --new starts over. Piped input is
attached to the question (git diff | ff fofo "review this"). Tool progress
goes to stderr, so stdout holds only the answer. Tools that need the web app
open (such as reading the open screen) report as unavailable, and FoFo
answers without them.
Examples
# Authenticate against a local backend
fongflow login --api http://localhost:9999
# In CI: run a workflow with input, fail the job if the run fails
fongflow flow run "Deploy preview" --input @payload.json --timeout 600
# What happened?
fongflow flow runs "Deploy preview" -l 5
fongflow flow show 0a50c5e7-…
Runner — run Flow steps on this computer
Prefer a single binary with no Node install?
runner/in this repository buildsfongflow-runner, a Rust port of this command with the same server protocol and the samerunner.json(seerunner/README.md). Everything below applies to both; replacefongflow runner <cmd>withfongflow-runner <cmd>.
fongflow runner turns this machine into a runner for the Flow app's
"Run on my computer" step. The runner only makes outbound HTTPS calls: it
long-polls for jobs, streams the script's output back, and reports the result,
so it works behind NAT with no open ports.
# 1. Flow → Runners → Add runner, then copy the key it shows (ffrun_…):
fongflow runner register --key ffrun_… --api https://your-host/api
# …or, when the CLI is logged in, let it create the runner for you:
fongflow runner register --name "My MacBook" --labels mac,home
# 2. Decide what this machine may run (shell scripts are OFF by default):
fongflow runner config --allow-shell on # any script
fongflow runner config --allow "git pull" # or only these commands
# 3. Run it
fongflow runner start # foreground
fongflow runner install-service # background, starts at login (macOS launchd / Linux systemd)
fongflow runner status # config + key check
fongflow runner test "git pull" --dry-run # would this machine run it, and where?
fongflow runner test "npm test" --image node:22 # run it the way a job would, locally
fongflow runner logs -f # the background service's log
fongflow runner jobs # recent jobs on this runner (needs `fongflow login`)
fongflow runner uninstall-service
fongflow runner unregister
Container mode
A step set to Run in: Container runs in a throwaway Docker/Podman
container: only the step's working directory is mounted (at /work,
optionally read-only), with memory/CPU limits, no-new-privileges, and
optionally no network. Containers don't need --allow-shell.
fongflow runner config --container-only on # never run anything directly on this machine
fongflow runner config --allow-image "node:*" --allow-image "fongflow/*" # image allowlist (empty = any)
fongflow runner config --default-image python:3.12-slim --memory 4g --cpus 4 --network off
fongflow runner build-image claude-code # builds fongflow/claude-code (Claude Code, non-root)
fongflow runner build-image claude-code-python # + pip, venv, uv (also claude-code-rust, claude-code-dotnet)
A credential picked on the step (Anthropic, OpenAI or GitHub) reaches the
script as ANTHROPIC_API_KEY / OPENAI_API_KEY / GITHUB_TOKEN + GH_TOKEN.
A Claude Code subscription token (from claude setup-token) arrives as
CLAUDE_CODE_OAUTH_TOKEN, so Claude Code runs on your Pro/Max plan.
The secret is fetched when the job is claimed, is never stored with the job,
and is masked as •••• in logs and output.
The runner's config lives in runner.json next to the credentials. Changes to
what it may run apply from the next job; no restart is needed. Scripts run in
your login shell ($SHELL -l -c) so they see your normal PATH. A script runs
as your user, with your permissions, so only allow shell on machines and
accounts you trust your workflows with.
Computer — a browser FongFlow AI uses for you
fongflow computer start opens a browser on this machine that FongFlow's AI
can drive: ask in chat with !computer … ("!computer find today's SJC gold
price"). It runs as this machine's runner, so register one first.
fongflow computer start # in a container if Docker/Podman is usable, else the installed Chrome
fongflow computer start --headed # the installed Chrome with its window visible, to watch it work
The container mode runs chromedp/headless-shell with its DevTools port on
127.0.0.1 only, and keeps the browser profile (cookies, sign-ins) in the
fongflow-computer-profile volume. The Chrome mode keeps its own profile in
computer/profile next to the credentials, apart from your everyday Chrome.
Open Computer (/computer) in FongFlow to watch the browser live next to
a chat with it (CLI 2.4.0+). Take over hands the browser to you — click,
scroll and type on the live screen to sign in or solve a check, while the AI's
changing actions wait — and Return control gives it back. Frames are only
streamed while that page is open.
At a sign-in the AI shows you a private form in the chat: what you type (or a login you saved for that website) goes straight to this computer, which fills the page — the AI never sees it. Permissions on the Computer page decide what it may do without asking: fill in, send/post, delete, buy/pay — each "Do it", "Ask me" (Approve / Deny in the chat) or "Leave to me".
In Flow, the Do a task in the browser action (computer.run_task) runs a
whole task unattended — on a schedule, say — and returns the answer. Keep
fongflow computer start running for that; steps that need approval or a
sign-in stop the task with the reason.
The AI reads pages as text and acts on numbered elements: open, click, type, press a key, scroll, back. It never types into password fields (the CLI refuses), and stops at sign-ins, payments and robot checks for you to finish in the browser. Like the runner, it only calls out to FongFlow, so it works behind NAT; Ctrl+C stops it and closes the browser.