@flowhunt/cli
v1.0.7
Published
FlowHunt CLI — connect your local computer to FlowHunt AI Projects and run tasks through Claude Code, Cursor, or Codex.
Downloads
1,083
Readme
@flowhunt/cli
The FlowHunt CLI connects a local machine to FlowHunt AI Projects. When a task in a FlowHunt project is routed to this machine's runner, the CLI hands the task off to a local coding agent (Claude Code or Codex CLI; Cursor in a follow-up) and reports progress back as issue comments.
Install
npm install -g @flowhunt/cliRequires Node ≥ 22 (the CLI uses the built-in fetch and node:fs
features).
Quick start
# 1. Authenticate. Opens your browser for OAuth (PKCE).
flowhunt login
# 2. Start the daemon. On first run it auto-registers this machine as a
# runner (using the current directory and your hostname as defaults)
# and then begins polling every 5s for tasks routed here.
cd ~/code/my-repo
flowhunt runner start --workspace <your-workspace-id>Subsequent runs only need flowhunt runner start — the runner identity
is already persisted to ~/.flowhunt/config.json. The standalone
flowhunt runner register command still works if you want to provision
without starting the daemon.
Pick the runner from the "Runner" dropdown when creating an issue in the FlowHunt web UI. The daemon will claim the task within 5 seconds, drive the local agent, and stream output back as comments on the issue.
Threat model & sandbox
The runner executes server-supplied task prompts on your machine, so trust between FlowHunt's backend and your laptop matters. The actual boundaries the CLI enforces:
- OS-level sandbox on the spawned agent. Each task gets a fresh
.claude/settings.jsonin--direnabling Claude Code's native sandbox (Seatbelt on macOS, bubblewrap on Linux/WSL2). The sandbox:- Writes are confined to
--dirand its subdirectories. - Reads are locked to
--dirplus system paths outside$HOME(/usr,/etc,/var,/opt— needed for compilers, package managers, and shared libraries). Everything else under$HOMEis denied:~/.ssh,~/.aws, browser cookie databases, colleague projects in~/code, the user's other repos, etc. Dev tools that normally read~/.config/<tool>or~/.gitconfigwill need per-project config (<repo>/.git/config,<repo>/.npmrc, etc.) or env vars. failIfUnavailable: trueis set, so a missing bubblewrap install fails the task loudly instead of silently downgrading to unsandboxed mode.
- Writes are confined to
- Bash deny rules for remote-shell and file-transfer primitives
(
nc,ssh,scp,rsync) whose only purpose is moving data off the box.curlandwgetare intentionally permitted — they're load-bearing for legitimate dev work, and the sandbox'sdenyReadabove protects the credential files an exfil attempt would actually need to access. - Path-traversal check on
submit_issue_artefact. The local stdio MCP server resolves every artefact path viarealpathand refuses anything that lands outside--dir. This stops a prompt-injected agent from uploading~/.ssh/id_rsaas a "screenshot." - Linux preflight on
runner start. Verifiesbubblewrapandsocatare onPATHbefore starting the daemon; if missing, printsapt-get install bubblewrap socat(or the dnf equivalent) and exits non-zero. - Codex CLI is spawned as
codex exec --json --ephemeral --skip-git-repo-check --cd <dir> --sandbox workspace-writewithCODEX_HOMEpointed at a per-daemon runtime directory (~/.flowhunt/runtime/codex-<pid>/, mode 0700) that carries aconfig.tomlpinningsandbox_mode = "workspace-write"andapproval_policy = "never", plus the FlowHunt MCP server block with the runner-scoped bearer (mode 0600). The user's~/.codex/auth.jsonis symlinked into the runtime dir so Codex's saved CLI auth still works without copying credentials, and--ephemeralkeeps session rollouts off disk. The bearer never lands inside the runner's--dir.
--dir alone is not the trust boundary — Claude Code's Bash tool
has no cwd jail at the model layer. The OS sandbox + deny rules are
what actually contain the agent. If you change --dir to a non-git
directory, the sandbox still applies.
Ubuntu 24.04+ AppArmor
bubblewrap on recent Ubuntu needs an AppArmor profile granting it user
namespaces. Without this, claude exits non-zero on every task. Run
once:
sudo tee /etc/apparmor.d/bwrap > /dev/null <<'EOF'
abi <abi/4.0>,
include <tunables/global>
profile bwrap /usr/bin/bwrap flags=(unconfined) {
userns,
include if exists <local/bwrap>
}
EOF
sudo systemctl reload apparmorSee the Claude Code sandboxing docs for the full setup.
Commands
flowhunt login
Opens the system browser to FlowHunt's OAuth consent screen. After
approval the browser redirects to a http://localhost:<port>/callback
URL the CLI listens on, captures the authorization code, exchanges it
for tokens, and persists them to ~/.flowhunt/config.json (mode 0600).
flowhunt logout
Best-effort revoke of the current token, then wipes the local config.
flowhunt runner register
Most users can skip this — flowhunt runner start --workspace <id>
auto-registers on first run. Use this command when you need to
provision a runner ahead of time (e.g. in a CI image build).
| Flag | Required | Description |
|---|---|---|
| --workspace <id> | yes | Workspace ID to register under. Find it in the FlowHunt URL. |
| --name <name> | no | Friendly label (e.g. "laptop", "CI box"). Defaults to the machine hostname. |
| --dir <path> | no | Path tasks should execute in. Defaults to the current directory. |
| --kind <kind> | no | Override the auto-detected agent: claude_code, cursor, codex, other. |
Internally this calls POST /v2/runners/register. FlowHunt creates the
runner row, issues a fresh runner-scoped OAuth token pair tagged with
the new runner_id, revokes the original user-scoped token, and
returns the new pair. The CLI overwrites ~/.flowhunt/config.json with
the runner-scoped tokens.
flowhunt runner list --workspace <id>
Pretty-prints every runner in the workspace.
flowhunt runner remove <runner_id> --workspace <id>
Revokes the runner. The matching oauth2_token row is marked revoked
server-side; if this machine was acting as the removed runner, the local
config is cleared too.
flowhunt runner start
The main daemon loop. If this machine isn't registered yet and you
pass --workspace <id>, it auto-registers before entering the polling
loop. Two independent timers run after that:
- Claim/poll (every 5s):
POST /v2/runners/{id}/claim. If a task comes back, run it through the adapter and post the result. - Heartbeat (every 30s):
POST /v2/runners/{id}/heartbeatwith statusonline(orbusywhile a task is in flight).
| Flag | Required | Description |
|---|---|---|
| --dir <path> | no | Path tasks should execute in. Defaults to the current directory. |
| --workspace <id> | no | Workspace ID — required only on first run, to auto-register. |
| --name <name> | no | Friendly name used at auto-register time. Defaults to the machine hostname. |
| --kind <kind> | no | Override the auto-detected agent kind at auto-register time. |
| --poll-ms <ms> | no | Poll interval in ms (default: 5000). |
| --heartbeat-ms <ms> | no | Heartbeat interval in ms (default: 30000). |
SIGINT / SIGTERM finish the current task gracefully then exit; a
second signal hard-kills.
On start the daemon writes its PID to ~/.flowhunt/runner.pid and
removes the file on clean exit. If the file already exists when you run
runner start, the command refuses to start a second daemon.
flowhunt runner stop
Reads ~/.flowhunt/runner.pid, sends SIGTERM, waits up to 10s for a
graceful shutdown, then escalates to SIGKILL. Cleans up the pid file.
Safe to run when no daemon is up — it just reports nothing to stop.
flowhunt runner status
Prints the API URL, runner ID, runner kind, and whether the local daemon process is alive (by pid-file lookup). A stale pid file (PID gone) is auto-cleaned.
On-disk config
Lives at ~/.flowhunt/config.json with mode 0600:
{
"apiUrl": "https://api.flowhunt.io",
"runnerId": "uuid",
"runnerKind": "claude_code",
"accessToken": "<jwt>",
"refreshToken": "<uuid>",
"expiresAt": 1737000000000
}The CLI never writes a long-lived API key here — accessToken is a
short-lived JWT and refreshToken is an opaque server-issued string,
both rotatable via revoke from FlowHunt's runners page.
Self-hosted FlowHunt
Set FLOWHUNT_API_URL or pass --api-url to point at your instance:
flowhunt --api-url https://flowhunt.example.com loginThe CLI requires https:// for non-loopback hosts so a typo or
phishing-shaped --api-url can't silently exfiltrate your tokens.
http://localhost and http://127.0.0.1 are accepted without further
flags for local development. For self-hosted FlowHunt behind a private
VPN where you can't terminate TLS, pass --allow-insecure:
flowhunt --api-url http://flowhunt.internal --allow-insecure loginUse that flag only when the network path between you and the self-hosted instance is fully trusted.
Development
cd cli
yarn install
yarn build
node dist/index.js --helpyarn dev runs tsc --watch.
