sub-bot

v0.10.3

Self-hosted Rust agent server with sessions, async tasks and signed webhooks

Other bots call a local coding agent.
This is the server.

sub-bot is one Rust binary for macOS and Linux. A bot sends a prompt over HTTP, a Unix socket, or stdio JSONL. The server keeps that session apart from every other session, runs the coding tools in that session’s directory, and can POST a signed webhook when an async task finishes.

  • brew tap dickwu/tap
  • brew install dickwu/tap/sub-bot

The tap README also lists brew install sub-bot after the tap is installed. MIT or Apache-2.0.

Session events a caller reads
  1. event: agent_start
  2. event: tool_execution_start
  3. event: tool_execution_end
  4. event: agent_settled
  5. data: {"aborted":false}
  6. Async task webhook
  7. X-SubBot-Event: task.completed
  8. X-SubBot-Delivery: …
  9. X-SubBot-Signature: t=…,v1=…

The binary

One process, called by other bots.

It is not tied to a repository. You give each bot a token, and the token maps to the profiles that bot may use. Sessions stay isolated. Each one has its own working directory, and that directory has to sit inside the profile’s workspace roots.

The built-in tools are read, write, edit, ls, grep, find, and bash. grep and find link ripgrep inside the process, so the host does not need rg. TLS is rustls. There is no OpenSSL dependency. Windows is not a supported target.

The procedure

A call, in order.

  1. Install the binary

    On macOS or Linux, run brew tap dickwu/tap and then brew install dickwu/tap/sub-bot. The formula covers arm64 and x86_64. A release tarball is the other route, and cargo build --release -p agentd builds it from the source repo.

  2. Point a bot at it

    sub-bot serve listens on 127.0.0.1:7878. GET /v1/health needs no token. Everything else expects Authorization: Bearer plus the bot’s token. --unix-socket serves the same routes, and a user listed in localClients can call that socket with no token. sub-bot rpc is the stdio door: one JSON command per line. Off this machine, put TLS in front. The server does not trust X-Forwarded-For.

  3. The session stays in its file

    A streamed POST /v1/chat sends server-sent events until agent_settled. The transcript is appended to that session’s JSONL as it happens. After a restart, the same client loads the id again. A different client gets 404. A run that was in flight is not started over.

The parts

What the server actually does.

Tools
read, write, edit, ls, grep, find, and bash. Paths stay relative to the session directory. Absolute paths and .. are refused. preset:readonly drops write, edit, and bash. clock and http_fetch stay off until a profile lists them. bash strips provider keys and SUB_BOT_TOKEN from the child environment.
Sessions
Each session is one JSONL file, mode 0600. serve, run, and rpc write it as messages arrive. The file layout follows pi’s session tree. DELETE /v1/sessions/{id}?purge=true removes the file. Without purge, the JSONL stays and the id can be loaded again.
Tasks
POST /v1/chat with async set to true returns 202 and a task id. The caller can disconnect. Poll GET /v1/tasks/{id}, or pass a callbackUrl whose host is allowed. stream: true cannot be combined with async: true.
Webhooks
The server POSTs JSON. Headers are X-SubBot-Event, X-SubBot-Delivery, X-SubBot-Timestamp, and, when a secret file is set, X-SubBot-Signature. The signature is HMAC-SHA256 over the timestamp, a dot, and the raw body. Delivery is at least once. Dedupe on the delivery id.
Approval
toolApproval is never, always, or a rule set. ask pauses the tool until the caller approves or denies over HTTP, RPC, or the terminal. A session can tighten the profile’s rules. It cannot loosen them. The default is never.
Keys
sub-bot keys set reads a key and does not print it. macOS stores it in the login keychain. Linux writes a mode 0600 file under ~/.config/sub-bot/keys. No API returns a key. Logs do not record one. From 0.10.3 the keychain entry is written with /usr/bin/security.
Providers
OpenAI and Anthropic load side by side. faux needs no key and makes no network call. With no config file, the OpenAI default model is gpt-6.1-sol, and the built-in profile allows gpt-6.1-sol and gpt-6-astra. An Anthropic session with no model uses claude-opus-5-5.
MCP
A profile may attach stdio or streamable HTTP MCP servers. The model sees those tools under names shaped like mcp__<server>__<tool>. Only tools are connected. A crashed server restarts on the next use, up to five times in five minutes.
Issues
Problems are appended to issues.log as JSONL and can be posted to alert targets. Keys, tokens, and webhook secrets are redacted. A 4xx response is not, by itself, an issue.

Get a copy

Install it, then talk to it.

Homebrew

Documented in the sub-bot README and the formula. The formula’s homepage is the GitHub repo.

brew tap dickwu/tap
brew install dickwu/tap/sub-bot

Example config: $(brew --prefix)/share/sub-bot/sub-bot.example.json. After the tap is installed, brew install sub-bot also works.

On this machine, no token

With no config file, sub-bot uses one client and one profile, both named default. The workspace is the current directory, or --workspace. The server maps its own user id to that client. ask talks to the server. It does not read the API key itself.

sub-bot keys set --provider openai
sub-bot serve --unix-socket
sub-bot ask "hello"

Add --provider faux to serve offline, with no key. TCP still wants a token: pass serve --token or set SUB_BOT_TOKEN. If you omit both, the random token is not printed.

A token for another bot

Keep the token text for the bot. Put the printed hash in that client’s tokenSha256. Copy the example config, then edit profiles and clients. The README has the full file.

sub-bot hash-token "$(openssl rand -hex 24)"
cp "$(brew --prefix)/share/sub-bot/sub-bot.example.json" sub-bot.json
sub-bot --config sub-bot.json serve --bind 127.0.0.1:7878

Release archive

Download from v0.10.3 and check the .sha256 file published next to it. Put sub-bot on PATH and keep sub-bot.example.json.

PlatformArchive
Linux x86_64sub-bot-v0.10.3-x86_64-unknown-linux-musl.tar.gz
Linux arm64sub-bot-v0.10.3-aarch64-unknown-linux-musl.tar.gz
macOS Intelsub-bot-v0.10.3-x86_64-apple-darwin.tar.gz
macOS Apple Siliconsub-bot-v0.10.3-aarch64-apple-darwin.tar.gz
xattr -d com.apple.quarantine sub-bot

macOS may block the first launch. That command clears the quarantine flag on the binary.

From source

git clone https://github.com/dickwu/sub-bot.git
cd sub-bot
cargo build --release -p agentd

The README’s build command is cargo build --release -p agentd. The clone is the public repo.

3 November 2026

v0.10.3 keeps the keychain entry across upgrades.

Keychain items are read and written through Apple’s /usr/bin/security. The access list is no longer tied to the sub-bot binary, so a later brew upgrade does not ask for a new authorization. An entry created by 0.10.2 or earlier still needs one migration, from a terminal you can click: sub-bot keys reauth, or sub-bot keys reauth --from-env, or a fresh sub-bot keys set. Then restart the LaunchAgent once with launchctl kickstart -k gui/$(id -u)/com.dickwu.sub-bot. That step is once. Notes for the release are in the README and on the tag.

Answers

Questions

What is sub-bot?

sub-bot is a self-hosted coding agent server written in Rust. Other bots call it over HTTP and server-sent events, or over stdio JSONL. It is not tied to one repository. Each bot has a token, and that token maps to the profiles it may use.

Which machines does it run on?

macOS and Linux, on arm64 and x86_64. It ships as one binary. TLS uses rustls, so OpenSSL is not required. Windows is not a supported target. The Linux release archives are musl builds.

How do I install it?

Run brew tap dickwu/tap, then brew install dickwu/tap/sub-bot. Once the tap is installed, the tap README also lists brew install sub-bot. The formula installs the binary and an example config at $(brew --prefix)/share/sub-bot/sub-bot.example.json. Release archives are on GitHub.

How does another bot call it?

The server listens on 127.0.0.1:7878 by default. The bot sends an Authorization: Bearer token to POST /v1/chat. Set stream to true for server-sent events. The same routes are served on a Unix socket, and a user mapped in localClients can call that socket with no token. sub-bot rpc speaks one JSON command per line on stdin and writes events on stdout.

What happens to a session after a restart?

serve, run, and rpc write each session to its own JSONL file. When the same client asks for that id, the server loads the file. A different client gets 404. The working directory must still sit inside the profile's workspace roots. A tool call that was in progress is marked interrupted and is not run again.

Where do API keys live?

In a file, in an environment variable, or on macOS in the login keychain. sub-bot keys set stores a key and does not print it. On Linux it writes a mode 0600 file under the config directory. No API returns a key, and logs do not record one.

Which tools does the agent get?

The default set is read, write, edit, ls, grep, find, and bash. grep and find use an embedded ripgrep engine, so the host does not need rg installed. The readonly preset drops write, edit, and bash. toolApproval can allow, ask, or deny a call before it runs.

Is it free?

Yes. You may use it under the MIT license or the Apache License 2.0, at your option. Source, design notes, and releases are at github.com/dickwu/sub-bot.