Terminal picker for Claude Code, Codex, OpenCode and Pi: harness, provider, model, system prompts, launch.
Claude Code, Codex, OpenCode and Pi each want a different environment, and so does every endpoint you point them at, so shell wrappers duplicate that setup once per pair. Here it is one config file, and the model list is not config at all: it is fetched from the provider.
cargo install fastpickOr take the signed binary for your platform from Releases, make it executable and put it on your PATH. It is self-contained either way.
fastpick # menu
fastpick -p "hello" # menu, then those arguments go to the agent
fastpick --harness opencode # skip the first screen
fastpick --harness codex --provider acme --model acme-large # no menu at all
fastpick --key acme.openai # one credential of a site holding several
fastpick --list --provider acme # what that provider serves right now
fastpick --list --json # the same, for another program
fastpick --dry-run # the exact command and environment
fastpick --update # install the newest signed release
Each of --harness, --provider and --model skips its own screen; the menu opens on the
first one you left out. --key names a credential, which names its provider too, so it
skips that screen as well.
Up and down move, right goes forward, left goes back. On the model list Enter launches
straight away: the system prompt the model declares is already checked and the effort is the
model's default, so the usual case is one key. Right opens the options panel beside the
list, where space changes whatever the cursor is on. tab refetches the model list and
typing filters it.
Every .md in the prompts folder is offered for every model, and any of them can be ticked
for any model. Which one starts ticked is written in the config and nowhere else:
[[provider.model]]
id = "acme-orca-v4-pro"
prompt = "house-style" # the .md is optional, the case does not matterNothing is guessed from the model's name. A file used to be tied to the id it was named
after, which meant a prompt written for a task or a house style could not be given to two
unrelated models, a file named after nothing was invisible until a key was pressed, and an
endpoint routing by its own scheme (acme-orca-v4-pro for what is upstream an
orca-v4-pro) matched nothing at all, silently. Naming the file you want says it once.
A prompt naming a file the folder does not hold is reported, in the panel and at launch,
rather than dropped.
cargo install --git https://github.com/beboite/fastpick --locked
--locked builds against the lockfile in the repository rather than whatever resolves
today. A binary built anywhere else works just as well: it reads a config and starts a
child process, and has no install step of its own.
Run fastpick once: it writes a starter config and stops, because the providers it ships
are examples rather than endpoints. Editing that file is the whole configuration.
fastpick --edit # opens it in $VISUAL, $EDITOR, or your platform's default
fastpick --set-key acme # prompts, does not echo, writes owner-only
fastpick --paths # where everything lives, and who can read each key file
config.example.toml is that file, commented block by block:
harnesses, providers, per-harness bindings, model catalogues, local proxies, host checks.
Only the harnesses whose binary is on this machine are offered, so one config can follow you
across machines that do not have the same agents installed.
Paths accept ~, $VAR and %VAR%. Keys are referenced by file path, never inlined, and
never reach a config file, a command line or a log. Every variable an adapter owns is either
set or removed, so picking a third-party endpoint clears ANTHROPIC_API_KEY instead of
sending it there.
Some sites issue one key per upstream group, and the groups differ in which models they hold
and which API surfaces they allow. A provider can therefore declare several [[provider.key]]
blocks instead of one credential:
[[provider]]
id = "acme"
name = "Acme"
[[provider.key]]
id = "anthropic" # no `.` in it, see below
label = "anthropic" # written dim beside this key's models
auth_token_file = "~/.acme/anthropic.key"
[provider.key.harness.claude-code]
base_url = "https://acme.example"
[[provider.key]]
id = "openai"
auth_token_file = "~/.acme/openai.key"
[provider.key.harness.codex]
base_url = "https://acme.example/v1"
wire_api = "responses"
[[provider.key.model]]
id = "acme-gpt"Everything describing a route lives on the key: its token file, catalogue, bindings, models,
proxy and host check. The provider keeps id, name, group and an env each key layers
its own over. Leaving a route field on a provider that declares keys is refused at load, with
the field to move named, because a credential belonging to none of the keys is how the wrong
one reaches an endpoint.
The provider appears as soon as one key binds the harness, and the model list narrows to the
keys that do, so picking Codex above shows the openai key's models alone. The model picked
resolves the key, which is what keeps the endpoint and the token from coming out of two
different blocks, and a proxy declared on one key never starts for a launch on another. Each
key is asked its own catalogue, in parallel, and cached separately. On the command line a key
is <provider>.<key>, so fastpick --set-key acme.openai writes one credential and
fastpick --set-key acme errors and lists what it could have meant. --key reads the same
id and narrows the model list to that one credential, which is also how --model stays
unambiguous when two keys of a site serve a model of the same name. A provider holding a
single key keeps answering to its own id everywhere, and the short form above stays valid.
| Harness | Endpoint goes in as | Model | Extra instructions |
|---|---|---|---|
| Claude Code | environment variables | --model |
--append-system-prompt-file, appends |
| OpenCode | inline JSON in OPENCODE_CONFIG_CONTENT |
--model provider/model |
its instructions array, appends |
| Codex | dotted TOML overrides with -c |
--model |
not supported |
| Pi | a generated extension, loaded with --extension |
--provider and --model |
--append-system-prompt, appends |
Nothing writes to your agents' own config files, and OpenCode's inline config is merged over
opencode.json rather than replacing it. Codex gets no system prompt row because its
instructions override replaces the base prompt, tool rules included: swapping an agent's own
prompt for yours is not something to do quietly.
Pi is the one harness with no lever for an endpoint at launch: its providers carry their own
base url in code, and its models.json is your file inside its own config directory. So a
provider with a base_url is written as a pi.registerProvider extension under fastpick's
config directory, one file per route, rewritten at every launch and loaded with
--extension. That needs the endpoint's dialect, which is api on the binding. The key is
still a $FASTPICK_PROVIDER_KEY reference resolved at request time, so nothing generated
holds a secret.
Two Claude Code behaviours the config works around, both easy to get wrong by hand:
CLAUDE_CODE_MAX_CONTEXT_TOKENS does nothing for claude-* models, so fastpick refuses to
set it there; and ANTHROPIC_SMALL_FAST_MODEL defaults to a model most third-party
endpoints reject, which kills resumes and subagents until small_fast_model is set.
Every release ships one signed binary per platform, with the signatures collected in a
single SIGNATURES.json. fastpick --update checks the minisign signature against the key
compiled into the running binary before replacing anything. The
menu checks for a newer version once a day at most, on a background thread, and only ever
prints a line naming the version: nothing installs itself.
fastpick picks an agent and then becomes it, so a host that shows a tab per command ends up labelling a Claude Code session "fastpick". Right before the agent starts, fastpick says what it turned into, on the stream itself:
ESC ] 1337 ; boite ; launch = {"cmd":"fastpick","args":[…],"iconKey":"claude","label":"Claude Code"} BEL
args is the command line that reaches this same combination with no menu, which is the
useful half: a host that stores it reopens the session where it was rather than putting the
picker back in the way.
This is emitted only when TERM_PROGRAM names a host that asked for it, so every other
terminal sees nothing at all. OSC 1337 is a key=value channel several terminals already
share, and the boite; prefix means one that does not know this payload leaves it alone.
No credential is in it; it is the command line you could have typed.
cargo build --release
cargo testReleasing: bump Cargo.toml, then push a v tag that agrees with it. The workflow builds
five targets, signs each one, and opens a draft release for a human to publish.
MIT.