Embrace the entropy, probabilities favor the bold.
poisson is an opinionated coding agent you run in your terminal. It streams a real conversation, calls tools (bash, file read/write/edit, subagents), tracks every token and dollar, and keeps your whole history in a local SQLite database you own β just one static binary and your terminal.
It talks to Anthropic (Claude), OpenAI (ChatGPT subscription),
xAI (Grok, SuperGrok), OpenRouter (400+ models, API key), Ollama
(local + cloud), and llama.cpp (local llama-server) β paste images,
search past sessions full-text, compact context, approve risky shell
commands from a popup.
go install github.com/mq37/poisson/cmd/px@latest # needs Go 1.25+; installs to $(go env GOPATH)/bin
px login anthropic # or: px login openai | xai β skip for Ollama, no auth needed
px # launch the TUI
β οΈ Experimental. poisson is under active, fast-moving development β commands, flags, and behavior can change without notice between versions.
- Streaming REPL TUI β hand-rolled ANSI, no framework. Thinking blocks, tool cards with diffs, command palette (Ctrl+P), status bar with context % and cost.
- Real tools, not just bash β
read/write/edit/grep/glob,batch,web_search/web_ask/fetch(details),recall,set_title, subagents, and 13 built-in skills (below, user-extendable).bashis stateless β passworkdirexplicitly. - Recovers leaked
<invoke>tool calls β a weak/local model sometimes echoes other harnesses'<invoke>/<parameter>XML as plain text instead of a real call (never poisson's own format). Parsed back into a real, dispatchable call when it resolves cleanly; left as-is otherwise. - Bash safety guard, three speeds β Fast mode (default): deterministic
auto-approve for read-only commands, LLM risk-classifies the rest (low
auto-approves, medium/high/unknown asks you). The classifier also
auto-denies β no human asked β a command whose own output would leak a
secret to stdout (
echo $AWS_SECRET_KEY, an unredirecteddoppler secrets download, ...); switch to Paranoid mode to review and approve one yourself if that's a false positive. Paranoid mode asks for everything; installs/destructive/npx-style always ask. Shift+Tab cycles Fast β Paranoid β Yolo β every command runs immediately, no approval of any kind, any risk. Opt-in only, never the default; use on a box/ sandbox you're fine handing full unattended shell access to. - Secret redaction β tool output is scanned for secret-shaped text
(vendor tokens, PEM keys, JWTs, credential
KEY=VALUEpairs) and masked with[REDACTED BY HARNESS]before reaching the model, TUI, or session store. Best-effort, not a guarantee. - Subagents run async β one
subagenttool, dispatched byaction:spawn(default) starts a child and returns a job ID immediately; the main agent keeps working (or you keep chatting) while it runs in the background.statuspolls progress (one job or every job),resultretrieves the final output once,killstops one early (or every job at once) β see docs/async-subagent-plan.md. - Subagents on any provider β a spawned subagent inherits the main
session's model/effort by default; override either, or hand it a
provider/modelqualified ID to run it on a completely different provider. Same-provider overrides auto-run like today; a different provider asks you first by default, same popup as a risky bash command. List a set of providers under[subagent] trusted_providersinconfig.tomlto let them spawn on each other with no prompt β both sides of the pair must be listed, and trust skips only the popup, never the configured-provider and known-model checks. - Podman sandboxes β one
sandboxtool, dispatched byaction:creategives an isolated container (bashcalls passing itssandboxIdthen skip the approval gate entirely β the container is the boundary);cp/destroy/resurrect/listmanage it from any session, or via/sandbox ls/kill. Requirespodman. (details) - Sessions in SQLite β every message/tool/API call persisted, FTS5 full-text search, resume any session, auto-compaction when context fills up.
- Exact cost & tokens, live in the status bar and
/cost, plus live usage-limit tracking for Anthropic/OpenAI subscription accounts. - Image input β paste (Ctrl+V) or
@screenshot.png. (details) <render>file citations β cites a snippet (<render file="path" from="10" to="50"/>) instead of retyping it, zero output-token cost;ref="<commit-or-branch>"cites a git ref instead of the working tree. A citation that fails to resolve (bad path/ref) gets a couple of automatic, visibly-marked retries in the same turn before the answer is considered done.- Clickable
file:///http(s)://links β any such link in a reply (markdown[label](url)or a bare URL) renders as a real OSC 8 terminal hyperlink; supporting terminals (kitty, iTerm2, wezterm, ...) open it on click instead of you copying a path out and opening it by hand. Pairs with thecanvasskill below for HTML reports. - Message queueing β type while the agent works; sent at the next turn boundary instead of waiting for the whole turn to finish.
px orchestrateβ run persistent, isolated, headless poisson instances managed remotely over Telegram (one systemd-nspawn container per instance, one forum topic per instance), plus an optional unconfined host-direct instance kind for real approvals with no isolation. (details, host mode)
Fifteen skills ship baked into the px binary β no setup, no config
directory needed. The skill tool loads one by bare name (each is a
SKILL.md) and works the same for subagents as it does in the main session.
Fourteen live under the code/ topic group
(internal/skills/builtin/code/): code-quality, code-review, tdd,
feature-impact, review-pr, stacked-diff-review, check-work,
council, grilling, create-issue, create-pr, sandbox,
create-skill, ponytail β covering code review, TDD discipline,
blast-radius impact analysis, independent self-verification, multi-persona
critique, issue/PR drafting, and shipping the smallest correct diff.
canvas (internal/skills/builtin/canvas/) is ungrouped, since building a
self-contained HTML report/dashboard instead of a markdown wall applies to
any task, not just code.
Skills nest one directory deep for topic grouping: <name>/SKILL.md
(ungrouped) or <group>/<name>/SKILL.md. Group is purely organizational β
it changes the directory and the system-prompt listing (grouped skills show
as a bare-name line under one group description, instead of each carrying
its own description), never lookup or invocation.
Add your own under ~/.poisson/skills/<name>/SKILL.md or
~/.poisson/skills/<group>/<name>/SKILL.md β a user skill with the same
name as a built-in one overrides it regardless of group, so you can
customize any of the thirteen without touching the binary. /reload
rediscovers user skills without restarting.
Requires Go 1.25+. Everything else is vendored by the module graph. The
quickstart's go install above puts px at $(go env GOPATH)/bin/px β make
sure that's on your PATH. Building from a local clone instead (e.g. to hack
on it) still works: ./build.sh (compiles CGO_ENABLED=0 -> ./px).
Authenticate a provider (stored in ~/.poisson/auth.json, mode 0600):
px login anthropic # Claude Pro/Max β browser OAuth (subscription billing)
px login openai # ChatGPT Plus/Pro β browser OAuth (Codex subscription)
px login xai # SuperGrok β browser OAuth
px login openrouter # plain API key from https://openrouter.ai/keys
px login ollama # local Ollama at http://localhost:11434 (no auth needed)
# llamacpp: local llama-server at http://localhost:11212, no auth needed either
# anthropic/openai on a headless/SSH host: add --manual to paste the auth
# code instead of waiting on a local browser callback that can't be reached.
px login anthropic --manualThen just:
px # interactive TUI
px sessions # list past sessions
px resume <id> # open the TUI resumed straight into a past session
px cost # total spend
px version
# Headless one-shot (no TUI). Risky bash is denied unless --yolo (for *you*
# in a real shell only β the agent cannot nest it via the bash tool).
px -p "summarize this repo"
px -p --yolo "run the test suite and fix failures"Anthropic (Claude, OAuth or api_key), OpenAI (ChatGPT/Codex
subscription, OAuth), xAI (Grok, SuperGrok OAuth), OpenRouter (400+
models, API key), Ollama (local daemon or cloud, no auth), and
llama.cpp (local llama-server, no auth). Each ships a curated default
and tracks that provider's latest model lineup β any <provider>/<model-name>
works even before poisson has a built-in entry for it (see
Adding a custom / unlisted model).
llamacpp/Ollama share the same wire format; alpaca
discovers cached GGUF models and launches llama-server with sane defaults.
Custom provider instances: [custom_providers.<name>] defines a second
(third, ...) Ollama-compatible endpoint under a name you pick β e.g. a
daemon on a remote host, alongside your local one:
[custom_providers.bastion] # a second Ollama instance, any name
type = "ollama" # only "ollama" supported today
base_url = "http://bastion-host:11434"
model = "laguna-s-2.1:q4_K_M" # optional β omit to discover live via /api/tagsWorks everywhere a built-in provider does: /providers, /model,
px -p bastion/<model>, subagent provider pinning. No login needed (same as
ollama/llamacpp). Curate a model with the same [models.<name>."<model>"]
table a built-in provider uses (context window, effort levels, vision).
Switch anytime: /model, /effort, /providers (or the Ctrl+P palette).
Reasoning effort levels: low Β· medium Β· high Β· xhigh Β· max (default medium;
each model advertises which it supports).
Everything lives in ~/.poisson/ (created on first run, mode 0700):
| File | What |
|---|---|
~/.poisson/config.toml |
all settings (created with commented defaults on first run) |
~/.poisson/auth.json |
OAuth tokens / API keys (mode 0600) |
~/.poisson/poisson.db |
SQLite: sessions, messages, tool/API calls, FTS index |
config.toml is written for you with every option commented out β uncomment to
override. The knobs (each [<provider>].model sets that provider's default
model β the one used when it's the active provider, or when you switch to it
via /providers without naming a model):
# Default model as "<provider>/<model>" β sets the provider and its model in one
# line (a bare "<model>" applies to the default provider). Overrides the
# per-provider settings below.
# model = "anthropic/claude-sonnet-5"
# Reasoning effort for every request (low | medium | high | xhigh | max)
# effort = "medium"
[provider]
# default = "ollama" # anthropic | ollama | xai | openai | openrouter | llamacpp
[anthropic]
# model = "claude-opus-5" # or claude-sonnet-5 / claude-fable-5-1 (all adaptive-reasoning)
# classifier = "claude-sonnet-5" # bash-risk classifier for this provider
# api_key = "sk-ant-..." # optional; OAuth (auth.json) preferred
[openai]
# model = "gpt-5.6-terra" # via ChatGPT Codex subscription (px login openai)
# or gpt-5.6-sol / gpt-5.6-luna / gpt-5.5
[xai]
# model = "grok-build" # via SuperGrok subscription (px login xai)
[openrouter]
# model = "deepseek/deepseek-v4-flash-0731" # via API key (px login openrouter)
# api_key = "sk-or-..." # optional; prompted by px login openrouter
# base_url = "https://openrouter.ai/api/v1"
[ollama]
# base_url = "http://localhost:11434"
# model = "glm-5.2:cloud"
[llamacpp]
# base_url = "http://localhost:11212" # local llama-server instance
# model = "unsloth/Laguna-S-2.1-GGUF"
# [custom_providers.bastion] # a second Ollama instance, any name
# type = "ollama" # only "ollama" supported
# base_url = "http://bastion-host:11434"
# model = "laguna-s-2.1:q4_K_M" # optional β omit to discover live via /api/tags
#
# [models.bastion."laguna-s-2.1:q4_K_M"] # same [models.*] schema as any built-in provider
# context_window = 262144
# [subagent]
# trusted_providers = ["anthropic", "openai"] # skip the approval popup spawning subagents between these
[classifier]
# model = "" # fallback bash-risk classifier (bare = all providers, "provider/model" = one)
[compaction]
# threshold = 0.85 # compact when context passes this fraction
# reserve_tokens = 16384 # ...or this much headroom is left, whichever is hit first
# model = "" # summarizer model or provider/model (default: session model)
[tui]
# theme = "dark" # dark | light
# show_tokens = true # context % in the status bar
# show_cost = true # $ in the status bar
# Pricing (per 1M tokens) and model metadata (context window, effort levels,
# vision) overrides β see "Adding a custom / unlisted model" below for the
# full [pricing.*] / [models.*] example.Any model = "<provider>/<model-name>" works right away, even if poisson has
no built-in entry for it β you just get a generic fallback context window and
no effort levels/vision/adaptive-thinking. Two optional config.toml blocks
fill that in:
# 1. Teach poisson the model's real capabilities (shows up in /model, and
# gates which reasoning-effort levels the picker offers).
[models.ollama."glm-5.2:cloud"]
context_window = 200000
effort_levels = ["low", "medium", "high"]
vision = true
# 2. Teach it what the model costs, so /cost and the status bar aren't
# silently $0. Ollama already defaults every model to $0 (built-in
# wildcard, since most are local); Anthropic/OpenAI/xAI have no such
# wildcard, so an unlisted model on those three shows $0 cost until you
# add its rates here.
[pricing.ollama."glm-5.2:cloud"]
input = 0.5
output = 2.0Both blocks key on the exact model name (quoted, since it usually contains
./:), under [models.<provider>."<model>"] / [pricing.<provider>."<model>"].
Pricing (only) also matches by prefix if the key ends in * β e.g.
[pricing.ollama."*"] is the built-in fallback that prices every unlisted
Ollama model at $0; model metadata overrides always need the exact name.
Bottom-bar keys (input focus):
Enter send Β· Tab switch input/conversation focus Β· Ctrl+V image
Ctrl+F find Β· Ctrl+P palette Β· Ctrl+L effort Β· Ctrl+T fold thinking
Ctrl+E expand tool Β· Ctrl+M model picker Β· Ctrl+S session picker
Ctrl+B /btw prompt Β· Ctrl+R/Ctrl+N step input history
Ctrl+G finish subagents now Β· Shift+Tab cycle fast/paranoid/yolo approval mode
Esc cancel running turn Β· Ctrl+C clear input (twice to exit)
Click-drag selects text (auto-scrolls past the edge); Ctrl+Y copies to the system clipboard via OSC 52 (works over SSH) β plain Ctrl+ because most terminals already claim Ctrl+Shift+C for their own copy action.
Slash commands: /help /status /subscriptions /model /effort
/classifier-model /providers /sessions /resume /search /new
/clear /name /compact /cost /reload /sandbox /btw
/openai-reset-usage /quit.
Type @ to fuzzy-attach a file (or @image.png for an image).
/classifier-model picks which model rates bash-command risk for the
approval gate β usually worth pointing at something small and fast, since the
answer is one word and an expensive session model otherwise pays its own rate
per gated command. Mid-turn only unless made permanent via that provider's own
classifier = "..." in config.toml (see the [anthropic] block above).
Resolution order: /classifier-model pin β [<provider>] classifier β
[classifier] model β the session's own model.
poisson has 3 direct dependencies (modernc.org/sqlite, golang.org/x/term,
golang.org/x/image); everything else is stdlib.
- Single static binary β
CGO_ENABLED=0, one file, copy it anywhere. - Local-first & private β your data lives in
~/.poisson/poisson.db. No telemetry, no analytics, no phone-home. - Suckless-ish β simplicity over features, delete-before-add, readable code.
- Tested without the network β the suite mocks every provider; it never makes a real API call.
Runs persistent, isolated, headless poisson instances on a dedicated host,
managed remotely from a Telegram group β one forum topic per instance. Full
design/rationale: docs/orchestrator-plan.md,
docs/orchestrator-host-mode-plan.md.
Two instance kinds:
| Kind | Command | Isolation | Approvals |
|---|---|---|---|
| Box (default) | /new, /new-box |
systemd-nspawn container, real root minus CAP_SYS_MODULE, host networking |
yolo (auto-approved β blast radius is one disposable container) |
| Host-direct | /new-host |
none β runs directly on the orchestrator host | real Telegram approval round-trip, always |
Host-direct is functionally the same as SSHing into the orchestrator host
yourself and running px there. It's opt-in at the config level and requires
an explicit confirmation flag on every single invocation β see Β§4/Β§5 below.
Every instance also gets a send_file tool: the agent can attach a local
file to its output and it's delivered as real Telegram media in the
instance's topic β images render inline (sendPhoto), everything else comes
through as a downloadable document (sendDocument), up to 20MB.
- A Linux host you're comfortable dedicating to this (systemd,
arm64oramd64), reachable over SSH as root. Referred to below as<host>β substitute your own hostname or IP everywhere you see it. systemd-nspawn/machinectl/systemd-run(box instances only βapt install systemd-containeron Debian/Ubuntu).- A Telegram bot: message @BotFather,
/newbot, save the token. In Bot Settings β Group Privacy, turn privacy off (so the bot sees every message, not just ones addressed to it). - A Telegram group with Topics enabled (this auto-promotes it to a supergroup) β the group where you'll run everything. Add the bot as an admin with "Manage Topics" and "Post/Delete Messages" permissions.
Post any message in the group, then:
curl -s "https://api.telegram.org/bot<TOKEN>/getUpdates" | jq .Read message.chat.id (a large negative number for a supergroup) and
message.from.id (your own numeric Telegram user id) from the response.
Neither is secret on its own, but treat them the same as any other
credential-adjacent config value β don't paste them into a public issue/PR.
ssh root@<host> 'apt install -y systemd-container
mkdir -p /var/lib/machines /var/lib/px-orchestrate/instances'Build a golden rootfs once β every box instance is cloned from it:
ssh root@<host> 'machinectl pull-tar https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-arm64-root.tar.xz px-golden'(use the amd64 tarball on an x86_64 host). Inside the image, install what
instances need and disable the guest's own network management (nspawn's
--resolv-conf=bind-host handles DNS instead):
ssh root@<host> 'systemd-nspawn --directory=/var/lib/machines/px-golden --bind-ro=/etc/resolv.conf -- \
bash -c "apt update && apt install -y git ripgrep ca-certificates curl jq && \
systemctl disable systemd-networkd systemd-resolved systemd-networkd.socket && \
passwd -l root && mkdir -p /root/.poisson /work"'CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o px ./cmd/px # arm64; amd64 for an x86_64 host
scp px root@<host>:/usr/local/bin/px.new
ssh root@<host> 'mv /usr/local/bin/px.new /usr/local/bin/px && chmod 755 /usr/local/bin/px'Provider credentials for host-direct instances (and for px orchestrate
itself, which needs its own session) come from /root/.poisson/auth.json on
<host> β px login <provider> there, or copy over one provider's entry
from your own ~/.poisson/auth.json (never the whole file to a shared host).
Box instances get their own scoped copy automatically, generated per
instance β see AuthorizedProviders in docs/orchestrator-plan.md.
Append to /root/.poisson/config.toml on <host>:
[orchestrator]
chat_id = -1001234567890 # from step 2, negative
allowed_user_ids = ["123456789"] # from step 2 β everyone else is silently ignored
allowed_models = ["anthropic/claude-sonnet-5"]
default_model = "anthropic/claude-sonnet-5"
max_instances = 5 # box instances only
state_dir = "/var/lib/px-orchestrate"
image = "px-golden"
# Host-direct instances β leave both at their defaults (off/unlimited-once-
# allowed) unless you specifically want /new-host available:
# allow_host_instances = true
# max_host_instances = 0 # 0 = unlimited once allowedThe bot token itself does not go in config.toml β see the next step.
ssh root@<host> 'install -m 600 /dev/stdin /etc/px-orchestrate.env <<EOF
POISSON_TELEGRAM_TOKEN=<your bot token>
EOF'
scp deploy/px-orchestrate.service root@<host>:/etc/systemd/system/
ssh root@<host> 'systemctl daemon-reload && systemctl enable --now px-orchestrate'Verify: ssh root@<host> 'HOME=/root px orchestrate --config-check' prints
the fully resolved config (token shown as present/missing only, never the
value itself). --dry-run (no root/systemd-nspawn required) smoke-tests the
command dispatch against fake instances β nothing real gets created.
/new [name] [repo-url] create a box instance (alias for /new-box)
/new-box [name] [repo-url] same, explicit
/new-host [name] refused with a warning + the exact re-invocation
/new-host [name] --confirm-unconfined-host
actually creates a host-direct instance
/list every instance's name/status/model
/status this instance's status, cost, sessions
/model <provider/model> switch this instance's model (next turn)
/suspend / /resume power off (keep state) / power back on
/kill permanently destroy this instance
/approve / /deny [reason] resolve a pending bash-approval (host instances only)
/cancel stop the current turn
Send a plain message (no /) in an instance's own topic to give it a task.
poisson Β· run px Β· /help for the tour
