Skip to content
Fredrin
Documentation

Workers

fredrin-agent - setup and pairing guide

fredrin-agent is the standalone daemon that runs Fredrin tickets on your own always-on machine (a home server, a cloud VM, a spare Mac). It dials out to Fredrin over Ably — no inbound ports, no VPN — receives dispatched tickets, runs your coding CLI in an isolated per-ticket worktree, and streams the live terminal back to the ticket UI.

You never need Fredrin's source code: the agent ships as a single self-contained bundle downloaded by the installer.

Prerequisites

On the machine that will run jobs:

  • Node.js 20+ (includes npm, which the installer uses to fetch a prebuilt terminal engine — no compiler or root required on common platforms).
  • GitHub CLI (gh) installed — Fredrin uses it to open PRs when Workers ship. It does not need to be signed in, and signing it in is not a setup step: Fredrin supplies the GitHub credential with every job. Signing it in changes which GitHub account this machine's jobs run as — read Which GitHub account jobs run as first.
  • A supported coding CLI, installed. For Claude Code a machine with its own sign-in just uses it (the local login always wins); a machine without one — headless boxes and containers included — is covered by the workspace's subscription token (minted once with claude setup-token, stored in Settings → Models), delivered per-job. The other CLIs still need their own sign-in on the machine:
    • Cursor Agent — agent login (default)
    • Claude Code — claude /login, or no sign-in when the workspace has a subscription token
    • Codex CLI — codex login (with ChatGPT)
    • GitHub Copilot CLI — npm i -g @github/copilot
  • A clone of your project repo on the machine — jobs run in worktrees created from this checkout. Note its path; you'll enter it during pairing.
  • curl or wget (macOS/Linux). On Windows PowerShell's own irm does the job, so there is nothing to install.

On Windows the installer supplies most of that itself. install.ps1 installs Node.js 20+ and gh for you (via winget, falling back to choco) when they are missing, and leaves gh signed out — which is what you want, see Which GitHub account jobs run as. What it does not install is Git for Windows, and that one is not optional: every job runs inside a bash -l login shell, so a machine without it pairs, reports healthy, and then fails every ticket at spawn. See Windows below.

Run the agent as a non-root user where the machine has one: Claude Code refuses --dangerously-skip-permissions — the flag every unattended Worker runs on — under root. On a box whose only account is root (a microVM, a container, a cloud image at first boot) that is not a choice, so the setup prompt below resolves it instead of warning about it: it declares the sandbox (IS_SANDBOX=1), and fredrin-agent service install writes the same declaration into the supervisor unit whenever the daemon will run as root. See Running as root.

Which GitHub account jobs run as

This is the one rule to know before you set a machine up, and it is written here only — everything else on this page, and the setup prompt below, links back to it.

Fredrin sends a GitHub credential with every job — the credential of whoever dispatched the ticket. So an agent machine needs no GitHub secret of its own, and a fleet box, a container or a cloud image can hold none at all.

A gh login on the machine outranks it. If gh auth login has been run here, every job this machine runs acts as that GitHub account, no matter who dispatched it. When that account can see every repo behind the workspace's projects, nothing goes wrong and you may never notice. When it cannot, GitHub answers a repo it is not allowed to see with not found rather than forbidden, so the tickets for that project all die at clone —

git clone https://github.com/<owner>/<repo> failed:
GraphQL: Could not resolve to a Repository with the name '<owner>/<repo>'

— naming a repository that exists and that the dispatching user can see perfectly well. The machine stays green in Settings → Remote agents the whole time, because the wrong identity is only reached once a job starts.

So: install gh and leave it signed out, unless you deliberately want this machine pinned to one GitHub account. If it is already signed in and jobs are failing at clone, gh auth logout and fredrin-agent restart hand the machine back to the per-job credential.

For the mechanism itself — the full credential ladder, and the four states Fredrin's readiness probe reports — see GitHub CLI readiness.

1. Install

The quickest way is to let Claude Code do it for you. On the machine you want to run jobs (Claude Code is one of the supported coding CLIs, so it's often already there — or SSH into the machine first), paste this prompt into Claude Code:

Set up the Fredrin remote agent on this machine so it can run my Fredrin tickets. Do it for me end to end:

1. FIRST, identify the Fredrin installation for this server. One installation runs ONE daemon serving EVERY workspace paired to that server; production and staging use separate installations. Pairing is additive. Check whether fredrin-agent is already installed or running, verify it is up to date, and update it when it is not. Keep an existing healthy installation running.
   - Find the launcher with `command -v fredrin-agent`; default state is under `~/.fredrin`. Named profiles have their own launcher under `agents/<profile>/bin` and their own launchd or systemd name.
   - Run the selected launcher's `status` and `service status`. Use that SAME launcher for every remaining command; generic `fredrin-agent --profile <name>` also selects its installed bundle. If the intended profile, owner, or server is ambiguous, ask me to identify it before changing anything.
   - Before introducing profiles under one uid, every running daemon and repair CLI must support `installation-protocol` version 1. Older doctor commands cannot distinguish installations. Use separate service users if that prerequisite is not met. Profiles do not isolate arbitrary Worker code or third-party credentials.
   - Check the build with `fredrin-agent update --check`; `fredrin-agent version` reports installed bytes. If it is ALREADY up to date: leave it running — do NOT uninstall, remove, or reinstall it. Skip the prerequisites and installer (steps 3–4), run the platform check (step 2), then verify (step 5).
   - If a newer build is available (the bundle is STALE): update it IN PLACE with the selected `fredrin-agent update`, then continue at step 2. Drain that installation's active work before `fredrin-agent restart`; other installations keep running. Windows stages the pair and applies it on the next start. Preserve agent.json, credentials and machine-id.
   - If update fails, inspect selected `status`, `logs`, and `doctor`. A mixed-origin migration-required result needs the operator runbook at /docs/remote-agent, not a reinstall. For a necessary reinstall, drain only this installation, run its `service uninstall` before `stop`, then verify its supervisor and owned PID are stopped. Unknown processes require operator attribution; never terminate processes by a machine-wide name match.
   - A new named installation requires an explicit FREDRIN_AGENT_PROFILE and FREDRIN_URL. FREDRIN_AGENT_WORKER_LIMIT is OPTIONAL: leave it unset for no limit, which is the default, and set it only if I give you a budget measured from host capacity. Its native installer preserves the global default launcher and installs a private bundle. If no installation exists, continue at step 2.
2. Check WHICH USER will run the agent, before installing anything — it decides whether this machine can run ANY Claude ticket. Run `id -u`:
   - It printed something other than 0 (a normal account): there is nothing to do here — go straight to step 3.
   - It printed 0 (you are root): every unattended Fredrin Worker launches its coding CLI with `--dangerously-skip-permissions`, and Claude Code REFUSES that flag under root unless a sandbox is declared. Left alone, this machine will install, pair and report healthy, and then fail every single Claude ticket before the CLI even starts. Fix it NOW, as part of this setup — do not ask me first, and do not wait to see the error:
     a. If this box has a normal login account, prefer it: `getent passwd | awk -F: '$3>=1000 && $3<65534 {print $1}'` lists the candidates. If there is one, run every remaining step as that user (`sudo -u <user> -i`) and skip b–c.
     b. Otherwise root is the only account — a microVM, a container, or a cloud image at first boot, where the VM/container boundary IS the isolation boundary. Declare that to Claude Code, for this shell and for future ones:
          export IS_SANDBOX=1
          grep -qs '^IS_SANDBOX=' /etc/environment || echo 'IS_SANDBOX=1' >> /etc/environment   # Linux; skip if there is no /etc/environment
     c. The supervisor gets it automatically: `fredrin-agent service install` (which `fredrin-agent login` runs for you) writes `Environment=IS_SANDBOX=1` into the unit whenever the daemon will run as root, and regenerates it on every install and self-update. So do NOT hand-write a systemd drop-in, and do NOT edit the unit file — an edit is exactly what a later self-update overwrites.
3. Check the prerequisites and install anything missing: Node.js 20+ (with npm) and the GitHub CLI `gh` INSTALLED. Do NOT run `gh auth login` on this machine, and do not tell me to: `gh` does not need to be signed in, because Fredrin supplies the GitHub credential with every job (the run executor's), so this machine can hold no GitHub secret at all. A login here would override that credential for every job — the rule and what it costs are at https://app.fredrin.com/docs/remote-agent#which-github-account-jobs-run-as. For the Claude runtime a machine with a signed-in Claude Code just uses that login; a machine without one cannot run Claude tickets, so sign Claude Code in here if this box will run any. Only if I plan to run Cursor or Codex tickets on this machine does that CLI need its own sign-in.
4. Install (or update) the agent by running the official installer — it downloads the CURRENT self-contained bundle and puts a `fredrin-agent` launcher on my PATH, replacing any older bundle:
     curl -fsSL https://app.fredrin.com/install.sh | sh
   (use `wget -qO- https://app.fredrin.com/install.sh | sh` if curl isn't available)
5. Verify it installed by running `fredrin-agent status` and show me the output.

Then tell me the next step: pairing this machine with my workspace by running `fredrin-agent login` (I'll grab the pair code and workspace id from Settings → Remote agents → Pair new agent). Pairing is additive — running `login` for another workspace ADDS it alongside the ones this machine already serves; it never removes them. If this machine was already paired and you only updated it in place, run `fredrin-agent restart` instead to bring the freshly-installed version online.

Claude Code first checks for an existing fredrin-agent: an up-to-date one is left running (one installation serves its paired workspaces, so adding another is additive), a stale one is updated in place, and only a missing one is installed fresh. It then resolves the root case (step 2 — a no-op unless you are root; see Running as root), checks the prerequisites, runs the installer if needed, and confirms the agent is on your PATH. Then you pair it in step 2 below. In the Pair new agent dialog, Fredrin hands you the same prompt with your pair code and workspace id already filled in, so Claude Code installs and pairs in one paste.

Prefer to run it yourself? The installer is a plain one-liner. Pick the one for the machine that will run jobs, which is not always the one you are reading this on:

# macOS / Linux
curl -fsSL https://app.fredrin.com/install.sh | sh
# Windows, in PowerShell
irm https://app.fredrin.com/install.ps1 | iex

Either way, this downloads the bundled agent to ~/.fredrin/agent/, installs the prebuilt terminal engine beside it, and puts a fredrin-agent launcher on your PATH. On macOS and Linux that is a /usr/local/bin symlink where the directory is writable and ~/.fredrin/bin otherwise — the installer prints which. It also warns if claude or gh are missing.

The Pair new agent dialog shows whichever of the two matches your platform, with a switch for the case where the machine you are pairing is not the machine you are looking at.

Windows

install.ps1 is the PowerShell twin of install.sh and lands in the same places, but a stock Windows box starts further back than a Linux one, so it does more:

  • It installs Node.js 20+ and gh itself, via winget and falling back to choco. Do not install them first; do not run gh auth login afterwards.
  • The launcher is fredrin-agent.cmd, in %USERPROFILE%\.fredrin\bin. Windows has no shebang handling and no PATHEXT entry for an extensionless file, so a bare fredrin-agent there would be a document — double-clicking it raises the "Select an app to open" picker rather than running anything. A POSIX twin is written beside it so the bare name also works in Git-Bash.
  • That directory goes on your user PATH (appended, never reordered) and on the current session's PATH, so fredrin-agent status runs in the same PowerShell window, with no restart and no sign-out.
  • Git for Windows is a hard requirement for running jobs. The agent runs every job's coding CLI inside a bash -l login shell — the launch command is POSIX-quoted and the per-ticket wrapper and lifecycle hooks are sh scripts — so without Git-Bash every ticket dies at spawn with exit code 127 on a machine that looks perfectly healthy on the board. Install it from https://git-scm.com/download/win. Do not substitute WSL: C:\Windows\System32\bash.exe is a different machine, and Fredrin will not use it.
  • login installs a supervisor here too, as it does everywhere else — a Task Scheduler task named FredrinAgent that starts the agent at boot and at logon and restarts it if it crashes. There is no fredrin-agent start to run after pairing, and none after a reboot. See Supervision.

Re-running the installer upgrades the bundle in place. It never touches ~/.fredrin/agent.json, so every workspace the machine is paired to survives.

2. Pair with Fredrin

  1. In Fredrin, open Settings → Remote agents and click Pair new agent. The dialog hands you one copyable prompt, and both credentials ride inside it: its fredrin-agent login step already carries them, and its last two lines spell out the 8-character pair code (like ABCD-EFGH) and your workspace id for reading off by hand. The macOS / Linux · Windows switch above the prompt picks which machine it is written for — it starts on your own platform, so change it if you are pairing a different one.

  2. On the machine, run:

    fredrin-agent login

    You'll be prompted for:

    • Pair code — the prompt's second-to-last line. It does not expire, so take as long as the machine needs; it is single-use, so mint a fresh one for the next machine
    • Workspace id — its last line
    • Repo root — optional; press Enter to accept the default. Pairing is machine-wide: the agent serves every project in the workspace and clones each project's repo on demand (a local checkout is only reused when its origin matches the ticket's project), so there is no project to pick here

    Scripting it? On a machine nobody is typing at — a provisioning script, an SSH one-liner, a Claude Code session — pass the answers as flags instead, and the prompts are skipped entirely:

    fredrin-agent login --code ABCD-EFGH --workspace your-workspace-id

    (FREDRIN_PAIR_CODE / FREDRIN_WORKSPACE_ID in the environment do the same.) Do not try to pipe answers into the interactive prompts: a pipe hits EOF, readline drops everything after the first line, and login exits 0 having paired nothing.

  3. On success, login installs the agent as a service (a launchd LaunchAgent on macOS, a systemd user unit on Linux) so it starts at boot/login and restarts within seconds of a crash — and starts it. On an interactive terminal it asks first (default Y); non-interactive logins install without asking. Pass --no-service to skip and manage the process yourself.

  4. The dialog confirms the moment pairing succeeds, and the machine appears in the Remote agents list.

Pairing is additive. One machine runs one fredrin-agent daemon that serves every workspace it is paired to. Running fredrin-agent login for a new workspace ADDS a pairing — the workspaces you paired earlier keep claiming jobs uninterrupted; running it again for a workspace you already paired just rotates that one credential in place. fredrin-agent status lists every workspace this machine serves and whether each is online. (Older agents held a single pairing, so pairing to a second workspace used to overwrite the first — that is fixed.)

Credentials for every pairing are stored at ~/.fredrin/agent.json (mode 0600) as a pairings array; a legacy single-pairing file is migrated automatically on first read. The server URL is always app.fredrin.com; FREDRIN_URL exists only as a dev/preview override.

3. Manage the running agent

After login the agent is already running under supervision — there is no separate start step. Manage it with:

fredrin-agent status    # running? paired as what? where are the logs?
fredrin-agent logs      # last 200 log lines (-f to follow)
fredrin-agent restart   # supervised: supervisor respawns it; bare: stop + start
fredrin-agent stop      # graceful shutdown (releases in-flight jobs)

If you opted out with --no-service, fredrin-agent start from an interactive terminal detaches into the background and returns immediately — closing the SSH session doesn't stop it. Nothing restarts a bare daemon after a crash, and start says so on stderr.

Use fredrin-agent start --foreground to keep it attached (for debugging). Under systemd or pm2 it detects the supervisor and stays in the foreground automatically, so service units keep working unchanged.

The agent connects, probes which coding CLIs are installed, reports them to Fredrin, and waits for jobs. Pick the default CLI and model per workspace in Settings → Runtimes. When a ticket is dispatched to this machine, the agent creates a worktree and branch for the ticket, runs the configured CLI in a real interactive terminal, and streams output (and your typed input) through the ticket UI.

Restart the agent (fredrin-agent restart) after installing a new CLI, or after changing this machine's gh login, so it re-reports its capabilities.

Supervision (installed by login)

fredrin-agent login installs the service by default (see step 2 above). The service verbs manage it directly — including installing later if you opted out at login:

fredrin-agent service install     # start at boot + restart on crash
fredrin-agent service status      # supervisor state (plus plain daemon state)
fredrin-agent service uninstall   # stop supervising; removes the unit

Logs still land in ~/.fredrin/agent.log (fredrin-agent logs). On Linux the unit runs in your user session; service install prints the loginctl enable-linger $USER command that makes it start at boot before you log in. Re-running service install is safe — it regenerates the unit with the current node/agent paths.

On a headless Linux box with no user session (a VM you only reach over sudo, or one nobody logs into), service install repairs the missing XDG_RUNTIME_DIR when a user manager is running, and otherwise installs a system-wide unit at /etc/systemd/system/fredrin-agent.service — still running as your account, and starting at boot without lingering. That needs root once, non-interactively: run sudo -v && fredrin-agent service install. See Agent service supervision.

Running under your own systemd system unit or pm2 still works unchanged — the agent detects those supervisors and stays in the foreground. (pm2 start fredrin-agent -- start also works.)

Windows: Task Scheduler

service install registers a Task Scheduler task called FredrinAgent, plus the .cmd it runs — %USERPROFILE%\.fredrin\fredrin-agent-service.cmd, which is what service status reports as the unit path. Task Scheduler is preferred over NSSM for the same reason launchd is on macOS: it is on every Windows install and needs nothing downloaded.

The task runs as you, unelevated — never as SYSTEM, which would have neither your ~/.fredrin pairing nor your CLI logins. The .cmd is the restart loop: Task Scheduler only restarts a task that fails, and a supervised self-update exits 0 on purpose, so a restart-on-failure policy alone would leave the machine dark after every update.

Two things are worth knowing:

  • Where it starts from. The task is registered "run whether I am logged on or not" (S4U), which is what gives it a boot trigger — the Windows equivalent of loginctl enable-linger. That needs the Log on as a batch job right, which a standard account may not have; when the registration is refused, install falls back to a logon-only task and says so. Check which one you got with fredrin-agent service status — it prints either Starts at: boot and login or Starts at: login only.
  • service uninstall removes the task first, then stops the daemon. In the other order the restart loop would simply bring it back.

Running as root

Claude Code refuses --dangerously-skip-permissions under uid 0 unless a sandbox is declared, and that flag is what every unattended Worker launches with. So a daemon running as root with nothing declared pairs, reports healthy, and then fails every Claude ticket before the CLI startsfredrin-agent status is green the whole time, because the refusal only bites once a job is dispatched.

Where the machine has a normal login account, run the agent as that account and there is nothing to do. Where root is the only account — a Firecracker microVM, a container, a cloud image at first boot — the VM boundary is the isolation boundary, and service install declares it for you: the unit it writes carries

Environment=IS_SANDBOX=1

whenever the daemon will run as root. It is regenerated on every install and on every self-update, so unlike a hand-written drop-in it cannot be silently dropped — do not edit the unit or add a fredrin-agent.service.d/ override yourself. sudo fredrin-agent service install is unaffected: the unit still runs as the invoking account, which is not root, and gets no declaration.

To check what the running daemon actually has (Linux):

tr '\0' '\n' < /proc/$(cat ~/.fredrin/agent.pid)/environ | grep -c '^IS_SANDBOX=1$'

1 is correct. 0 means the daemon predates the declaration — run fredrin-agent service install and fredrin-agent restart.

Self-update

Once installed, the agent keeps itself current: while idle it checks Fredrin for new builds, swaps in the new bundle atomically, and relaunches. You install once; there is no manual upgrade step.

On Windows the swap is deferred rather than skipped: the validated bundle is staged next to the running one, and the next fredrin-agent start applies it before the daemon opens anything, then re-launches onto it. So the box still updates unattended — it just does the file replacement in the one window Windows allows it. fredrin-agent update there prints "Staged … — restart to load it" instead of "Updated". The thing that provides that next start is the Task Scheduler task above: an unsupervised Windows box stages an update it never applies. See Agent self-update.

Troubleshooting

  • Disk fills with old Worktrees — the Agent cleans completed/archived tickets at startup, hourly, and after runs. It removes proven clean checkouts and evicts known ignored build caches when source, missing upstreams, or unknown ignored files require keeping the checkout. fredrin-agent logs shows [worktree-gc] outcomes and the path/reason for failures; an unavailable server retains work until a later pass. Active and Review Worktrees remain intact. Unknown ownership and orphan folders require explicit attribution; see Worktree retention. The existing fredrin-agent worktrees gc --dry-run command reports server-eligible candidates, before Git safety checks decide removal or cache eviction.
  • "Not paired. Run: fredrin-agent login"~/.fredrin/agent.json is missing or unreadable; re-run fredrin-agent login.
  • Pair code rejected — a code is single-use, so one that already paired a machine is spent, and a mistyped one never existed. Neither is fixed by waiting or by retrying the same code: read it off the prompt again, or mint a fresh one from Settings → Remote agents → Pair new agent. A code does not go stale on its own, so if the machine is simply not there yet, keep the code.
  • service install fails with "Failed to connect to bus: No medium found"systemctl --user needs a user D-Bus session, and your shell has none: sudo/su create no login session (so XDG_RUNTIME_DIR is never set), and a box nobody logs into never starts a user systemd at all. Pick one: sudo loginctl enable-linger $USER and re-run; sudo -v && fredrin-agent service install to install a system-wide unit instead; SSH in as a real session rather than sudo su; or sudo apt-get install -y dbus-user-session. The command prints these itself. If fredrin-agent status says "unit file written but NOT registered with systemd", this is why.
  • Terminal engine install fails — your platform may lack a prebuilt node-pty binary; install build tools and re-run the installer (Debian/Ubuntu: sudo apt-get install -y build-essential python3).
  • Jobs dispatch but the CLI hangs or PRs never open — check that the coding CLI is signed in (claude /login, agent login, …) as the user the agent runs as; logins are per-user. gh needs no login of its own — Fredrin sends the credential with the job.
  • Every job on this machine dies at clone with "Could not resolve to a Repository" — the repo exists and you can see it, so this machine is acting as a different GitHub account: it has its own gh login, and that account cannot see the repo. gh auth logout and fredrin-agent restart hand it back to the per-job credential; see Which GitHub account jobs run as under Prerequisites.
  • A newly installed CLI isn't offered — restart the agent (fredrin-agent restart) so capabilities are re-probed.
  • --dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons — every Claude job fails instantly, either with that from the CLI or with Fredrin's own "Claude Code refuses --dangerously-skip-permissions under root". The daemon is running as uid 0 with no sandbox declared. Re-run fredrin-agent service install, which writes the declaration into the unit, then fredrin-agent restart, and re-check with the command under "Running as root" above.

Last updated