Repository navigation
Running as a Service
For regular use, install Keryx as a background service. uv run keryx serve is fine for
local testing, but it stops with the terminal and doesn't come back after a reboot.
Without a service manager, it also can't restart itself.
Both installers render a template from ops/ into your own service directory, and
keryx setup offers to run the right installer for you. It's safe to run an installer
again to apply template changes.
The installer takes two things from the shell you run it in, so run it from the shell whose tools the tasks should have:
-
PATH. A service manager starts its units with a barePATH, so the installer writes its ownPATHinto them. Tasks then find what a terminal finds (nvm, Homebrew, conda, cargo,~/.local/bin…). Run the installer again after yourPATHchanges. -
Where things are. The installer asks
keryx config path --shell, so it sees the directories exactly as the service will. It writesKERYX_HOMEand the fourXDG_*_HOMEvariables into the units. The units log toSTATE_DIR/logs/(~/.local/state/keryx/logs/by default), wherekeryx restartreads them back. Run the installer again after you move any of those directories.
CLAUDE_CONFIG_DIR isn't one of them. Keryx honors it wherever it reads the Claude CLI's
files: the stored login, CLAUDE.md, the skills, and the Slack MCP server's config. But
the units don't include it, so the service and its tasks use ~/.claude unless the
service's own environment sets it.
scripts/install-systemd.sh # render + start both units
scripts/install-systemd.sh --uninstall # stop + remove themThis renders two units into ~/.config/systemd/user/:
-
ops/systemd/keryx.service, which runsuv run --project <repo> keryx serve; -
ops/systemd/cloudflared.service, the tunnel tolocalhost:$PORT.
It enables both, and turns on lingering with loginctl enable-linger, so they keep
running when nobody is logged in and come back after a reboot. Both restart on failure.
The units' stdout and stderr go to STATE_DIR/logs/{keryx,cloudflared}.{out,err}.log,
and to journalctl --user -u keryx. Keryx's own log is STATE_DIR/logs/keryx.log
(10 MB × 5 rotated files).
If the named tunnel doesn't exist yet, the installer stops early and prints the three
cloudflared commands that create it. CLOUDFLARE_TUNNEL names the tunnel (default
keryx).
scripts/install-launchd.sh # render + load both agents
scripts/install-launchd.sh --uninstall # unload + remove themThis renders two agents into ~/Library/LaunchAgents/ and hands them to
launchctl bootstrap:
-
ops/launchd/dev.keryx.agent.plist, which runsuv run --project <repo> keryx serve; -
ops/launchd/dev.keryx.tunnel.plist, which runsngrok http --domain=<PUBLIC_HOST> <PORT>.
Both have RunAtLoad and KeepAlive, so they start at login and restart if they die.
launchd's own stdout and stderr go to STATE_DIR/logs/{keryx,ngrok}.{out,err}.log.
Keryx's own log is STATE_DIR/logs/keryx.log (10 MB × 5 rotated files).
On macOS the tunnel uses ngrok, where Linux uses cloudflared. A reserved ngrok domain
doesn't need a DNS zone, which makes setup simpler on a laptop. The installer needs
ngrok on PATH. To set it up once:
-
Install ngrok and add your token from the ngrok dashboard:
brew install ngrok ngrok config add-authtoken <your-token>
-
Reserve a domain in the ngrok dashboard.
-
Set it with
uv run keryx config set PUBLIC_HOST <that-domain>, or in the phone section ofkeryx setup.
CLOUDFLARE_TUNNEL isn't used on macOS. scripts/dev.sh still runs cloudflared on
either platform. keryx doctor accepts whichever tunnel program it finds.
If you rename the agent label, set SERVICE_UNIT. It defaults to dev.keryx.agent.
SERVICE_MANAGER picks the service manager: auto, systemd, launchd, or none.
SERVICE_UNIT names the unit or agent label (by default keryx.service or
dev.keryx.agent). If you set systemd or launchd, Keryx uses it as given.
With auto, Keryx checks whether this process actually runs as the unit, not just
whether systemctl or launchctl is on PATH. Under systemd, it checks the process's
own cgroup. Under launchd, it checks whether XPC_SERVICE_NAME equals the label.
A keryx serve you started in a terminal isn't supervised, even on a machine where the
service is installed too. So if you ask it on a call to restart itself, it refuses.
Otherwise it would restart the installed copy in its place, or a unit that isn't there.
keryx restart and keryx doctor always run in a terminal, never inside the unit. For
them, the question is whether the unit is installed, and keryx restart restarts that
unit. If there's nothing to restart, that's the same as none. That's a supported mode
with one consequence, described in Assumptions and Deployments.
- Restarting Keryx — what happens when the unit is asked to restart, and why Keryx phones you back.
- Troubleshooting — where the logs are, and what the common failures look like.
User documentation for Keryx.
For the threat model, see SECURITY.md;
for settings, the coding agents, and the tools, see docs/.
Apache-2.0, Copyright 2026 William Koch.
Setting up
Running it
Security and data
Tools and advanced features