@firefunc-agent/runner
v0.9.14
Published
FireFunc self-hosted runner — an always-on daemon that runs YOUR local coding agent (Claude Code, Codex, Gemini CLI, Cursor CLI) on work FireFunc routes to it, opens pull requests, and reports back. Your machine, your subscriptions.
Maintainers
Readme
firefunc-runner
Run your own coding agent — Claude Code, OpenAI Codex, Gemini CLI or Cursor CLI — on work FireFunc routes to you, and open PRs automatically.
This is the self-hosted runner for FireFunc's Claude Code Channel engine. It's a tiny always-on daemon on your machine (laptop, dev box, CI host) that:
- long-polls FireFunc (outbound only — works behind NAT/VPN, no inbound ports),
- claims a bug FireFunc dispatched to your project,
- runs your local coding agent (
claude -p,codex exec,gemini, orcursor-agent— whichever the work is routed to) in an isolated git worktree off a clean base branch, - opens a draft PR and reports the result back.
Why this and not the cloud Routine? No ~15/day cap (it uses your Claude subscription's limits), your code never reaches FireFunc — it goes from this machine to your own repository, and nowhere else — and it has your full local context — running app, AWS creds, gh/git auth, MCP servers, CLAUDE.md. What the runner does send is listed, field by field, under What leaves your machine.
From 0.9.0 the package also installs firefunc, the command line (and Claude Code
skill) for setting agents up. See firefunc: agents from a terminal, a script or Claude Code.
Requirements
- Node ≥ 20
- Claude Code installed and signed in (
claudeon your PATH) gitand the GitHub CLI (gh) signed in, with push access to the repos- A local checkout of each repo FireFunc will fix
Quick start
# 1. Install the package. Use -g, not npx — see the note below.
npm install -g @firefunc-agent/runner
# 2. In FireFunc: Engines → "Add a machine". Copy the WHOLE command it shows —
# it already contains your one-time token (ffr_…) and your API URL:
firefunc-runner connect ffr_xxxxx --api https://your-firefunc-host --repos-dir ~/code
# (or map a repo exactly: --repo acme/api=/Users/me/code/api)
# 3. Go online — starts at login/sign-in and survives reboot:
firefunc-runner installnpm install -g, not npx. npx runs the package from a throwaway cache
(~/.npm/_npx/<hash>/…) that npm is free to delete. A background service has to
point at a path that still exists after a reboot, so firefunc-runner install
refuses to run from there and tells you this. Everything else — connect,
start, status, logs — works fine under npx if you prefer.
--api is required: this package has no hard-coded FireFunc host, so it only
ever talks to the deployment you point it at.
That's it. When your QA files a bug (labeled for FireFunc), it appears here within seconds, gets fixed, and a draft PR shows up — all visible in the FireFunc dashboard.
Safety
- Works in a throwaway git worktree off a clean base — it never touches your working tree or current branch.
- Opens a PR only when there's a real diff (no phantom PRs).
- Titles the commit and the draft PR with one conventional prefix (0.9.2):
test:when every path the change touches is a test (a test file, a test folder's content, a test runner's config — a reproducer's failing test, a test writer's suite), otherwisefix:. Thefix:,feat:,chore:,refactor:,docs:andtest:prefixes the task's title already carried are taken off first, so a pull request raised from a FireFunc pull request is never titled "fix: fix: …". - Pushes a deterministic branch with
--force-with-lease, so a duplicate dispatch converges to one PR. A run that revises an existing pull request (0.8.1+) starts from that pull request's own branch and pushes without force, so git refuses rather than replace commits pushed while it worked. - Scans what it is about to push for secrets, on this machine, and refuses the push on a key, a token or a private key (0.8.3+) — see The secret scan before a push.
- Refuses to push a commit that contains a
.envfile or the run's MCP config, whatever the agent committed. - Writes the agent's skills to
.claude/skills/<slug>/SKILL.mdand keeps them out of every commit by their real path (0.9.1+): when the repository keeps.claude/skillsas a link into a folder it tracks (.claude/skills -> ../.agents/skills), the skill lands in that folder and is kept out there — by a.gitignoreinside each skill folder (that worktree only; your clone's.git/info/excludeis not written), in what the runner stages, and at the push, which is refused if the agent committed one anyway. A folder already in the checkout under the same name — in any case, on a disk that ignores case — is never written into (the run sees the repository's, and the log names it), and a link that leads out of the checkout is refused: nothing is written outside the worktree. Runners before 0.9.1 appended.claude/skills/to your clone's.git/info/exclude; that line can be deleted. - A signed trail of every step, and runs that wait for a person (0.9.9). The runner records each step the engine takes — its type, timing, outcome and the paths and line counts it touched, never a line of code — as a hash-chained trail signed with this machine's own key, and sends it to FireFunc, which countersigns it. An agent may ask a person a question mid-run, or report its plan for approval before it works: the session is then parked here (worktree and session kept, nothing pushed) and resumed in the same worktree with the answer.
- An agent's own environment variables and reference files (0.9.8). When the agent carries them, the runner asks FireFunc for them for the run it holds (
POST /channel/runs/<id>/agent-envand/reference-files— answered only while that run is live for this machine). The variables are set in the engine's environment only — your setup and background commands never see them — and every value FireFunc marks secret is replaced by[secret NAME]in everything the runner sends to FireFunc (events, steps, results, a result kept to deliver later) and everything it logs or keeps (the terminal, the session log, the local viewer); a push whose added lines contain one is refused (agent_secret). The files are downloaded from addresses signed for minutes, checked against the size and SHA-256 FireFunc recorded, and written to.firefunc/reference/in the worktree by the skills' rules: never into a folder the checkout already has, a.gitignoreof*inside, excluded from staging and refused at the push. The brief tells the agent the variables' names (never a value) and where the files are. - Runs Claude with
--permission-mode bypassPermissions(the default; override withpermissionModein the config or per job) without--dangerously-skip-permissions, which hangs non-interactive daemons. Note what that mode means: the agent is not asked to confirm anything, so the bounds that matter are the ones passed alongside it —--disallowedToolsfor every tool the chain withheld, and--strict-mcp-configso the run's servers are the ones FireFunc chose rather than whatever the machine happens to have. - Claims no work an engine cannot run here (0.9.6). Claude Code refuses that mode as root or under sudo — the default user of most containers — and exits before doing anything, so a runner started as root used to fail every run it claimed. The runner now asks at start (and whenever it finds a newly installed CLI) whether each engine can run unattended as its user: one that cannot is not offered to FireFunc, which hands this machine none of its work, the log and
firefunc-runner statussay why with the fix, and the Machines page in FireFunc shows it too. A run that reaches it anyway is answered at once, without starting the engine, as the machine not being ready — not as the task's failure. The fix: run the runner as a normal user. - A per-job timeout (default 20 min) prevents a stuck run from hanging the daemon. An agent's own time budget (FireFunc sends it as
maxMinutes, 0.9.3) is honoured under thejobTimeoutMinutesyou set, and in its place when you set none; the engine is told how long it has. - Nothing a session starts outlives its job (0.9.3). The engine runs in a process group of its own, and before the worktree is removed — however the job ended — the runner stops that group and every process whose working directory or command line is inside the worktree, its scratch folders or the review skills'
ff-review-*-<checkout>copies of it, with their descendants; the copies are removed with the worktree. A session's backgrounded test run can no longer go on for hours in a deleted directory. - Cleans up after a stopped runner, and only after itself (0.9.3). Each run it claims is written to
held/in its folder; the next process to start reports the runs a gone process held as lost (so FireFunc can run them again at once), stops what they left running and removes their worktrees. At start, and whenevergit worktree addfinds one of its own leftover worktrees still holding the run's branch, it removes that worktree and tries once more. Only its own —<temp>/firefunc-XXXXXX— never yours. - One heavy command at a time per repository (0.9.3). Its own setup commands and dependency install take a per-repository lock (
locks/in its folder), and the engine is offered$FIREFUNC_HEAVY— the brief asks it to run a whole-project build, type-check or test suite as"$FIREFUNC_HEAVY" <command>— which waits on the same lock. Sessions on different repositories are untouched.heavyLock: falsein the config turns it off. - A plan's usage limit is not a failure (0.9.3). When the engine's own closing report says its plan stopped it (Claude Code's "You've hit your session limit · resets 3:50am"), the attempt is reported as stopped by the plan's usage limit with the reset time, the runner claims no work for that engine until then (
limits.jsonin its folder keeps that across a restart), and it keeps the session's worktree: when FireFunc runs the task again after the reset, the same session continues where it stopped (--resume) instead of starting over. A kept worktree is removed a day after its reset if the task never comes back. - A session that ends before its result comes is continued once (0.9.3). A headless session ends with the agent's turn, and a test it moved to the background is stopped with it. The brief says so; and when the engine's own stream shows a command it started still running at the close — or its closing words only promise a later check ("Waiting on the admission run.") — the runner continues the same session once to get the result. A session that ends that way again is reported as unfinished: its closing words are not sent as its answer and nothing it changed is pushed.
- The GitHub MCP token is renewed as the run goes on (0.9.3). FireFunc's token for the GitHub MCP server lasts an hour, and runs last longer, so the engine reaches that server through a relay on 127.0.0.1 (a random port and key) which adds the token itself and renews it through FireFunc before it is 55 minutes old or when GitHub answers 401. The token is then never written into the worktree.
mcpRelay: falsein the config writes it into the run's MCP config as before. - Stopping finishes the runs in flight (0.9.3). The first Ctrl-C or SIGTERM stops claiming and lets the runs finish — the machine keeps telling FireFunc it is alive and draining meanwhile, so it does not look offline; a second one stops their engines now, and each run reports how it ended. Under the background service, which is killed soon after its SIGTERM, the engines are stopped after ten seconds so every run can report first.
Commands
| Command | What it does |
| ----------------------------------------- | -------------------------------------------------------------------------- |
| firefunc-runner connect <token> [flags] | Save your token + settings (~/.firefunc-runner/config.json, mode 0600) |
| firefunc-runner install | Install as a background service — starts at login/boot, restarts on crash |
| firefunc-runner start | Run in THIS terminal (stops when you close it) — for watching it work |
| firefunc-runner stop | Stop the background service (stays stopped) |
| firefunc-runner start-service | Start the background service again |
| firefunc-runner logs | Recent runner output |
| firefunc-runner uninstall | Remove the background service |
| firefunc-runner status | Saved config, whether each engine can run unattended here, and the service |
| firefunc-runner --version | The version this build carries — the one it reports to FireFunc (-v) |
connect flags
| Flag | Default | Meaning |
| --------------------------- | ---------------- | -------------------------------------------------------------- |
| --api <url> | — (required) | Your FireFunc API URL — the copied command fills this in |
| --name <name> | hostname | Friendly runner name (shown in the dashboard) |
| --repos-dir <dir> | — | Base dir; repos are matched by name (acme/api → <dir>/api) |
| --repo <owner/repo=/path> | — | Map one repo to an exact checkout (repeatable) |
| --claude-bin <bin> | claude | Claude Code binary |
Always-on
firefunc-runner start runs in the foreground: it dies with the terminal and does
not come back after a reboot. That matters more than it sounds, because
FireFunc's queue is pull — nothing is pushed to your machine, the runner asks
for work every few seconds. A runner that is not running is not a slow runner;
it is an absent one, and work waits for a machine that will never ask again.
So install it once:
firefunc-runner install- macOS — a launchd LaunchAgent at
~/Library/LaunchAgents/com.firefunc.runner.plist - Linux — a systemd
--userunit at~/.config/systemd/user/firefunc-runner.service, plusloginctl enable-lingerso it starts at boot, not at first login - Windows — a Task Scheduler task (
FireFuncRunner) that starts at sign-in. No WSL, no admin rights, no service wrapper.
It starts at login/boot and restarts itself if it crashes. It does not restart after a deliberate stop, or after FireFunc revokes the runner's token — that is a clean exit, not a fault, and respawning it would just log the same rejection forever.
Per-user, not system-wide, on purpose: the runner uses your claude /
codex / gemini login, your checkouts and your SSH keys. A root daemon
— or a Windows Service, which is the same problem — would have none of those,
and giving it them is exactly the concentration of access FireFunc is designed to
avoid. That is also why Windows gets a Scheduled Task and not a Service: no admin
rights needed, and it runs as you.
On Windows the task overrides four Task Scheduler defaults that would otherwise break it silently: it refuses to start on battery and stops when unplugged (fatal on a laptop), it is killed after 3 days, and it stops when the machine goes idle.
firefunc-runner status # installed? running?
firefunc-runner logs # what has it been doing
firefunc-runner stop # stop it; stays stopped
firefunc-runner uninstall # remove the service entirelyTwo things the installer bakes into the unit, because a service has no shell
profile: your current PATH (or the agent CLIs are not findable and every job
fails with claude: command not found) and HOME/USERPROFILE (or they cannot
find their own logins). If you install a new agent CLI somewhere new, re-run
install.
Tested: the macOS path is verified end to end on a real Mac — installed, killed with
kill -9, and confirmed launchd brought it back. The Linux and Windows paths are unit-tested (unit/XML contents and everysystemctl/schtasksverb) but have not yet been run on a real machine of either kind. Please report anything that misbehaves.
What leaves your machine
FireFunc never clones, fetches or stores your repository. The change goes from this machine to your repository on GitHub — the push and the draft pull request — and nowhere else. This is everything runner 0.9.6 sends to the FireFunc API you connected it to:
While it waits for work (POST /channel/claim, every few seconds): its version,
hostname and platform; the engines it can run; the port of its local viewer; how many
sessions it runs at once (0.9.1+ — what FireFunc offered it, the largest "Runs at once"
among the projects it serves, or your --max-parallel, which overrides that); the
subscription usage its engines can read (percent used and when each window resets); and
the MCP servers this machine is signed in to — names, states, expiries and which login
holds each (the runner's own or Claude Code's), never a token. From 0.9.3 also: the ids of
the runs it holds (working on, or keeping a result for), the ids of runs the runner process
before it held and lost, and the plan limits it is waiting out (engine and reset time).
From 0.9.6 also: the engines installed here that cannot run unattended as the runner's user,
each with a word for why (blockedEngines, e.g. Claude Code and runs_as_root).
While it is finishing its runs before stopping, waiting out a plan limit on every engine
it has, or has no engine that can run unattended, the same request goes every 30 seconds as
a heartbeat that asks for no work (heartbeat, and draining while it stops).
While a run works (/channel/runs/<id>/events and /activity): the stage it is at
(cloning, fixing, pushing, pr_opened, a heartbeat every five minutes) with the
repository, the branch, the task's title FireFunc sent it or the pull request's URL, and
one line per step the engine takes — a verb and a name:
| The engine… | The step says |
| ------------------------------- | --------------------------------------------------------------------------------------- |
| reads or edits a file | its path, relative to the checkout (src/domain/orders.ts) |
| runs a command | its programs and subcommands only — npm test, git status, sed — never an argument |
| searches | where it looked (src/orders, src/**/*.test.ts), never the pattern |
| calls an MCP tool | the server and the action (Jira · get issue) |
| fetches from the web | the host, or the words of a web search |
| uses a skill | the skill's name |
| starts a sub-agent | the task it gave it, with code taken out |
| updates its to-do list | "updated its plan" |
| uses any other tool | the tool's name and, when it was given a path, the path |
| says what it is doing, finishes | its own words on one line, with code taken out |
A command's arguments are where a sed expression, a python -c body or a heredoc
lives, so they stay here; so does a search pattern, which is often a line of code. Every
step is cut to one line and has anything shaped like a credential redacted. The files, the
commands' output and the transcript never leave: the local viewer and
firefunc-runner logs show them. From 0.9.3 a step also says whether the engine marked
the call's result an error (failed — never what it returned) and whether an MCP tool it
called is one the run's config declares a write (write).
When it finishes (POST /channel/runs/<id>/result): the outcome; the pull request's
repository, number, URL, branch and title; the agent's closing summary (from 0.9.3 also
beside a change agent's "no code change" result, so its findings can be posted); a
failure's reason; and the secret scan (next section). From 0.9.3 also: the full id of the
commit the run started from (readHead); when a plan's limit stopped the engine, that and
when it resets (limit); that the session ended with its own command still running
(openCommandAtEnd); and, for a run the previous runner process lost, lost. A FireFunc
API older than one of these fields refuses a result that carries it; the runner then sends
the result again without the newest field, and the next, so an older API costs the new
fields and never the result.
The summary keeps its prose, its lists and short inline names (
parseDate,src/a.ts:14). A fenced or indented code block, a diff, a run of statements pasted without a fence, a<pre>block and an inline span that is a statement rather than a name are each replaced by a note, which names the file and line when the block said which:Fixed the rounding in src/total.ts:3. ```ts src/total.ts:3 return items.reduce((a, i) => a + i.price, 0); ```is sent as
Fixed the rounding in src/total.ts:3. [code kept on the machine — see src/total.ts:3]A failure's reason is the runner's own sentence and, at most, the one line of a tool's output that says what went wrong. A failed setup command is named by its program (
Setup command failed (exit 2): npm run build — src/total.ts:3:10 - error TS2322: …), and from 0.9.2 by the file name of the script an interpreter ran (zsh night-setup.sh, never its folder); when no line of its output reads like an error — a script's own "free disk is below the floor" — the last line it printed that is not code and carries nothing shaped like a credential is sent instead, with the machine's home folder written as~. An engine ended by a signal from outside — SIGTERM (exit 143) or SIGKILL (exit 137): a person, the operating system or a time limit on the machine — is sent as that (0.9.2), not as a crash: the runner stages, commits and pushes nothing for it, opens no pull request, and does not start the session again. A refused push by git's ownerror:,fatal:andhint:lines — what a pre-push hook printed stays here; a crashed engine by its error line, never the source line a stack quotes. The whole output is in the runner's log. A session the model's safety system declined (0.9.1+ — Claude Code's "safeguards flagged" / usage-policy API error) is sent as that, in the runner's own words, and neither the runner nor FireFunc runs it again. It is read from how the engine ended — Claude Code's closing report says it closed on an error and called it a refusal — never from what the agent wrote, so an answer that quotes a refusal or links the usage policy is sent as the answer.What the run spent (0.9.1 and newer), read from the engine's own closing report — Claude Code's
resultevent: the tokens by class (input, output, thinking, cache read, cache written) in total and per model id, the turns, the time the session's main loop was working and the time in model requests (summed over concurrent requests, so it can be longer), the engine's own estimate of those tokens at the API's list prices or at the organization's managed rates (not a bill; none is sent when the engine guessed the price), whether the engine was signed in with a plan or an API key, whether the plan drew on its billed extra usage, and its version. Numbers and names only, nothing the agent wrote; an engine that keeps no such count sends none, and a crash's zeroed report is not sent as a count. A session whose sub-agents run in the background closes once per stretch of its main loop: from 0.9.2 the turns and the working time are added over every stretch (0.9.1 sent the last stretch's alone), and the session's totals are read once. The plan's 5-hour and weekly percentages are not sent: today's Claude Code carries them only in an undocumented field.Credentials it asks for (
/git-credential,/mcp-credential) carry the run's id and, for MCP, the server's name — nothing else. FireFunc works out the repository and the account from the run. From 0.9.3 the GitHub MCP credential is asked for again as it nears its hour, or when GitHub refuses it, the same way.
FireFunc applies the same rules again when it stores what an older runner sent, so a machine you have not updated yet is held to them too.
The secret scan before a push
Before it pushes (0.8.3+), the runner reads what the push adds — the change as a whole
and each commit on the branch, because a key committed and then taken out is still
published by the push — with the same patterns FireFunc's Trust Engine uses. Every file
is read as the text that is pushed, including one .gitattributes marks as binary, and
no replace ref or graft in the checkout can show the scan a different commit:
| Kind | What it matches | The push |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------ | ---------- |
| aws_access_key_id | AKIA… / ASIA… access key ids | refused |
| aws_secret_access_key | aws_secret_access_key = … with a 40-character value | refused |
| github_token | ghp_, gho_, ghu_, ghs_, ghr_ tokens and github_pat_ fine-grained tokens | refused |
| slack_token | xoxb-, xoxa-, xoxp-, xoxr-, xoxs- tokens | refused |
| google_api_key | AIza… keys | refused |
| stripe_secret_key | sk_live_… / sk_test_… | refused |
| private_key_block | -----BEGIN … PRIVATE KEY----- | refused |
| jwt | three-part eyJ….eyJ….… tokens | refused |
| generic_assignment | password, passwd, secret, api_key, access_token or private_key set to a quoted value of 12+ characters | goes ahead |
AWS's two documented example keys (AKIAIOSFODNN7EXAMPLE and its example secret) are
nobody's credential and are let through.
- A refused push leaves nothing on GitHub: the commit stays on the local branch, and the run fails with what was found, where, and what to do — rotate a real credential and have the agent read it from the environment or your secret store, or ask for a placeholder that does not look like a live key — then run the task again.
- A
generic_assignmentdoes not stop the push — a test fixture takes that shape as often as a live value does — but it is reported, and FireFunc does not auto-merge a pull request with any finding: a person decides. - A run that continues a pull request (a revision of one that is already open) is
refused only for what it adds itself: the commits already on the branch are on GitHub,
and refusing this push would not take them back. The pull request is read whole as
well, from the base branch, and what that finds is reported with the result — so a key
in the author's commits, or in one pushed between two runs, still keeps FireFunc from
merging it. If the pull request cannot be read from the base branch, the scan is
reported without
head(below). - A change the runner cannot read is pushed unscanned and reported without a scan, which FireFunc treats as "nothing has looked", never as clean.
The result carries the scan as a path, a kind and a line per finding — never a character of the secret — and the commit it covers:
{
"status": "pr_opened",
"secretScan": {
"scanned": true,
"hits": [{ "path": "test/login.test.ts", "kind": "generic_assignment", "line": 12 }],
"head": "<the full id of the commit the push sent>"
}
}head says the scan read the whole pull request, from the base branch to that commit. It
is left out when nothing was pushed, when the branch no longer pointed at the scanned
commit after the push, and when a continued pull request could not be read whole.
FireFunc takes the scan as covering a pull request only while the pull request's head is
that commit: a commit pushed after the run — by a person, a bot or GitHub's Update
branch — was never scanned, and FireFunc does not auto-merge the pull request then
either. A runner older than 0.8.3 sends no scan at all, with the same
result.
Agents with no repository
An agent whose Works in is "No repository" arrives at the runner with no repo to clone.
The runner (0.8.0 and newer) runs the engine in a throwaway directory with the agent's MCP
servers, opens no branch and no pull request, and reports the summary the engine finished
with — that summary, and the agent's declared deliverables, are the run's result. An older
runner never sees such a run: FireFunc keeps it for a machine that can take it, and says
which machine needs npm i -g @firefunc-agent/runner@latest.
firefunc: agents from a terminal, a script or Claude Code
The package installs a second command, firefunc, for the person who sets agents up
rather than the machine that runs them. It drives FireFunc's public API with a
workspace API key (ffk_…, made in FireFunc → Settings → API keys), and it is how
Claude Code sets up FireFunc agents for you.
npm install -g @firefunc-agent/runner@latest # firefunc arrived in 0.9.0: check with firefunc --version
firefunc login # paste the key at the prompt — nothing you type is shown
firefunc templates list # look before you create
firefunc projects list
firefunc connections list --project "Web app"
firefunc agents create --project "Web app" --template sentry-fix-pr --name "Sentry fixer"Agents are created as drafts. agents create answers with the draft and a link that
opens it in the composer; a person reviews it there and activates it. firefunc agents
activate works only with a key made with the agents:activate scope. Without that
scope the API refuses it and says why. Installing or writing skills and setting a
project's standing instructions, repositories or claims likewise need a key made with
workspace:configure, which is off unless ticked — except that agents create --template
installs the curated skills that template names, as launching it from the gallery does.
| Command | Needs | What it does |
| ------------------------------------------------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------- |
| firefunc login / logout / whoami | any key | Save, forget, or show the key this machine uses |
| firefunc templates list | agents:read | Whole agents to start from |
| firefunc projects list | agents:read | Where an agent can live |
| firefunc connections list [--project …] | agents:read | The tool accounts a project can use |
| firefunc skills list | agents:read | Skills an agent can carry |
| firefunc agents list \| get <id or @handle> | agents:read | Agents, one or all |
| firefunc agents create --project … [--template …] [--name …] [--spec f.json] | agents:write | A draft, and its composer link |
| firefunc agents update <agent> --spec f.json | agents:write | Change only the fields passed (a live agent also needs agents:activate) |
| firefunc agents pause \| resume <agent> | agents:write | Stop or restart an agent taking work |
| firefunc agents activate <agent> | agents:activate | Switch a draft on |
| firefunc run <agent> --title … [--body …] [--watch] | tasks:write | Start a task on an active agent (what "Run now" does) |
| firefunc tasks list \| get <id> \| watch <id> \| states | tasks:read | Follow tasks; get and watch print the agent's answer (0.9.4) |
| firefunc skills directory | agents:read | The curated skills, and which are installed here |
| firefunc skills install <slug> | workspace:configure | Install a directory skill so agents may carry it |
| firefunc skills create --file SKILL.md / update <slug> --file … | workspace:configure | Write or change a skill of the workspace's own |
| firefunc projects instructions get \| set <project> --file f.json | agents:read / workspace:configure | A project's standing instructions (set replaces all five fields) |
| firefunc projects sources get \| set <project> --file f.json | agents:read / workspace:configure | What work a project claims (set merges kind by kind) |
| firefunc projects repos get \| set <project> --file f.json | agents:read / workspace:configure | A project's repositories (set adds only ones its GitHub account can see) |
| firefunc skill install [--dir …] | — | Write the Claude Code skill (below) |
Every command has --help, and each one takes:
--jsonprints the API's answer as it arrived, for scripts and agents. A refusal goes to stderr as{"error":{"status","code","message","fix","issues","details"}}:codeis the API's stable reason (missing_scope,unknown_agent,not_ready…) anddetailsits specifics.- Exit codes:
0done;1refused or failed, with the reason and the fix on stderr;2the command line was wrong. --api-urlsets the API address (defaulthttps://api.firefunc.com, orFIREFUNC_API_URL). Plainhttpis refused except tolocalhost, because every request carries the key. The keyloginsaved is sent only to the address it was checked against: pointed anywhere else, a command is refused (api_url_mismatch) until youfirefunc login --api-urlthere.
Where the key lives. firefunc login checks the key against the API before it
saves it to ~/.firefunc/config.json. The file is readable only by you (0600), in a
folder only you can open (0700) when login creates it; a folder that already existed
keeps its own mode. The CLI never prints the key. That file is
separate from the runner's ~/.firefunc-runner, so firefunc logout cannot take a
machine offline. logout forgets the key on this machine only: the key works until
it is revoked in Settings → API keys. In CI, set FIREFUNC_API_KEY instead of
logging in. It takes precedence over the saved key.
The Claude Code skill
firefunc skill install # writes ~/.claude/skills/firefunc/SKILL.mdThe skill tells Claude Code when to use firefunc and how. It looks before it
creates anything (templates, projects, connections and agents that already exist),
creates a draft, and hands you the composer link. It activates an agent only when
you asked for that agent and the key has agents:activate. It never asks for a key
in the chat, and it reports a refusal with the fix. firefunc skill show prints the
skill. A SKILL.md you have edited is kept unless you pass --force.
Licence
@firefunc-agent/runner is the client half of FireFunc, published so the machines
that do the work can actually install it. It is free to download and run against
a FireFunc account you are authorised to use — see LICENSE.
The FireFunc service itself (API, workers, dashboard, infrastructure) is proprietary and is not distributed here. This package contains only the daemon that runs on your machine: it invokes coding agents you installed and pay for, works on your checkouts, and reports results back. Your source code does not need to reach FireFunc for any of it to work.
FireFunc is a product of SoftHawker LLC, which licenses this package.
