Skip to content

Latest commit

 

History

30 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fastpick

Terminal picker for Claude Code, Codex, OpenCode and Pi: harness, provider, model, system prompts, launch.

Release crates.io License Stars Issues Platform Rust

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.

Install

cargo install fastpick

Or take the signed binary for your platform from Releases, make it executable and put it on your PATH. It is self-contained either way.

Use

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 matter

Nothing 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.

Installing it

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.

Setting it up

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.

One provider, several keys

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.

Harnesses

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.

Updating

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.

Saying what the session became

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.

Build

cargo build --release
cargo test

Releasing: 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.

License

MIT.

About

Terminal picker for Claude Code, Codex and OpenCode: harness, provider, model, system prompts, launch.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages