Provider selection phase removed; Anthropic driver→model_id is now hardcoded in the script and non-Anthropic profiles supply provider.env (MODEL_ID, ANTHROPIC_BASE_URL, ANTHROPIC_API_KEY_FILE). Test suite baseline changes 58 → 41 assertions: the 17 dropped assertions covered the removed provider-yaml selection phase, not retained behaviour. Includes refactor-derived memory: decisions (provider architecture, driver enforcement via --model), bash gotchas (read -rp under set -e, never eval user config), process lessons.
14 KiB
claude-profile
Purpose
Launch Claude Code with a named configuration profile and an engagement mode. The profile selects which ~/.claude-* config directory to use (and optionally embeds non-Anthropic provider config); the engagement mode bundles a driver model, subagent policy, async tolerance, and workflow stance.
Usage
claude-profile [OPTIONS] [PROFILE] [-- CLAUDE_ARGS...]
Arguments
| Argument | Default | Description |
|---|---|---|
PROFILE |
(interactive picker) | Name of a profile under ~/.claude-<PROFILE>/. Pass default to use ~/.claude/. |
CLAUDE_ARGS |
(none) | Arguments after -- are passed through to claude unchanged. |
Options
| Flag | Description |
|---|---|
--mode <NAME> |
Skip the mode picker and use the named mode (chat, quick, deep, hybrid, orch). |
--preset <NAME> |
Use a named preset from the active profile's presets.yaml. Sets profile, mode, and any preset-defined defaults in one shot. |
--project <NAME> |
Pre-select a project (skips the project question for modes that ask it). |
--dryrun, -n |
Print all resolved values and the command that would be executed; do not launch claude, do not write active-mode.env or last-mode. |
--help, -h |
Show usage. |
Mutual exclusion
--presetis mutually exclusive with--modeand--project— the preset defines all of those. Passing both is an error.- A positional
PROFILEcombined with--presetis allowed only if the preset'sprofilefield matches; mismatch is an error.
Behaviour
The script runs in three phases: profile selection, mode selection, launch. Each phase has an interactive path (prompt the user) and a non-interactive path (CLI flag or config file).
Phase 1 — Profile selection
- If
--preset <name>is given, read the preset from the global presets file (see "File contracts" below) and use itsprofilefield. Skip the rest of phase 1. - Else if
PROFILEis given as a positional argument, use it directly. - Else (interactive): list profiles by scanning
~/.claude-*directories, display a numbered menu, prompt for selection. Default selection isdefault(~/.claude/). - Resolve the profile to a directory:
default→~/.claude/; otherwise~/.claude-<NAME>/. - If the directory does not exist, error and exit 1.
- Export
CLAUDE_CONFIG_DIR=<resolved>. - Emit the WezTerm theme escape sequence for the profile (existing behaviour, unchanged).
Phase 2 — Mode selection
- If
--presetis given, use the preset'smodeand any other fields (project, async, autoloop). Skip the rest of phase 2. - Else if
--mode <name>is given, use it. - Else (interactive): list available modes by scanning
<repo>/data/claude-profile/modes/*.md, display a numbered menu with each mode'stagand the first sentence of its## Purposesection. Default selection is the contents of$CLAUDE_CONFIG_DIR/last-mode(if present), elsequick. - Validate the chosen mode file exists at
<repo>/data/claude-profile/modes/<name>.md. If not, error and exit 1. - Read the mode file's frontmatter to extract
driver,tag,async_ok,autoloop,plan_mode_auto,spec_driven,escalates_to.
Model ID resolution
The model ID is determined by combining the mode's driver field with the profile's optional provider.env:
- Map the driver to an Anthropic model ID using the hardcoded table:
| Driver | Model ID |
|---|---|
haiku |
claude-haiku-4-5-20251001 |
sonnet |
claude-sonnet-4-6 |
opus |
claude-opus-4-6 |
opus-1m |
claude-opus-4-6[1m] |
- If
$CLAUDE_CONFIG_DIR/provider.envexists, parse it. If it contains aMODEL_IDentry, use that instead of the hardcoded default.
This means Anthropic profiles need no extra config. Non-Anthropic profiles embed their model and API config in provider.env.
Project selection is intentionally NOT asked by claude-profile. The existing CLAUDE.md session-start flow handles project selection once claude is running. claude-profile accepts --project <name> as a non-interactive override (for presets and scripted launches), which writes CLAUDE_PROJECT into active-mode.env.
Phase 3 — Launch
- Write the resolved values to
$CLAUDE_CONFIG_DIR/active-mode.env(key=value, one per line, see "File contracts"). - Write the chosen mode name to
$CLAUDE_CONFIG_DIR/last-mode. - Read the mode file's prose body (everything after the frontmatter) and append it to the context that
context-loadproduces. The mode body becomes part of--append-system-prompt. - Set
CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1(existing behaviour). - Set
COLORTERM=truecolorfor proper colour support. - If
provider.envspecifiesANTHROPIC_BASE_URL, export it. - If
provider.envspecifiesANTHROPIC_API_KEY_FILE, read the key from the file and export it asANTHROPIC_API_KEY. Error if the file is missing. - Export any other entries from
provider.envas environment variables. - Execute
claude --model "<model_id>" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}". - On exit, the existing WezTerm reset trap runs.
File contracts
Mode files — <repo>/data/claude-profile/modes/<name>.md
YAML frontmatter (machine-readable) + markdown body (read by the driver model). Required frontmatter fields:
name: <short name, matches filename>
tag: <status-line tag, ≤8 chars>
driver: <haiku|sonnet|opus|opus-1m>
async_ok: <yes|no>
autoloop: <none|"/loop 2m /orchestrate"|other slash command>
plan_mode_auto: <yes|no>
spec_driven: <yes|no>
escalates_to: <none|opus> # used by statusline to render →Opus arrow
The body must include sections (in order): ## Purpose, ## Driver constraint, ## Subagent policy, ## Reasoning posture, ## Async policy, ## Workflow stance, ## Auto-fire at session start, ## Status line format, ## Escalation triggers, ## Out of scope, ## On context wipe (/clear). ## Named workflows available is optional.
Profile provider config — $CLAUDE_CONFIG_DIR/provider.env
Optional. Only needed for non-Anthropic profiles. Plain key=value format (no quoting, no shell expansion):
MODEL_ID=MiniMax-M2.7
ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic
ANTHROPIC_API_KEY_FILE=~/.claude-secrets/minimax-auth-token
ANTHROPIC_MODEL=MiniMax-M2.7
ANTHROPIC_SMALL_FAST_MODEL=MiniMax-M2.7
API_TIMEOUT_MS=3000000
Reserved keys with special handling:
MODEL_ID— overrides the driver→model_id mapping; passed toclaude --modelANTHROPIC_BASE_URL— exported as-isANTHROPIC_API_KEY_FILE— file is read and its contents exported asANTHROPIC_API_KEY
All other keys are exported as environment variables unchanged.
Active mode env — $CLAUDE_CONFIG_DIR/active-mode.env
Written on every launch, overwritten each time. Read by:
- The status-line script (to render the mode tag)
- CLAUDE.md session-start hook (to re-apply the mode after
/clear)
CLAUDE_MODE=deep
CLAUDE_MODE_TAG=deep
CLAUDE_DRIVER=sonnet
CLAUDE_ESCALATES_TO=opus
CLAUDE_PROJECT=cluster-bootstrap
CLAUDE_ASYNC_OK=yes
CLAUDE_AUTOLOOP=none
CLAUDE_PLAN_MODE_AUTO=yes
CLAUDE_SPEC_DRIVEN=yes
CLAUDE_MODE_FILE=/home/paul/dev/claude/small-scripts/data/claude-profile/modes/deep.md
Last mode — $CLAUDE_CONFIG_DIR/last-mode
Single line: the mode name. Used as the default in the next interactive picker.
Presets — $CLAUDE_CONFIG_DIR/presets.yaml
Per-profile, user-edited. Each preset is a named bundle of answers to the picker:
deep-cluster:
profile: oreillyit-anthropic
mode: deep
project: cluster-bootstrap
async: yes
quick-skills:
profile: oreillyit-anthropic
mode: quick
project: small-scripts
quick-minimax:
profile: oreillyit-minimax
mode: quick
project: small-scripts
The profile field is required. See data/claude-profile/presets.yaml.example for a full example.
Status line integration
The status-line script (~/.claude/status/statusline.sh) reads $CLAUDE_CONFIG_DIR/active-mode.env when CLAUDE_CONFIG_DIR is set. It uses CLAUDE_DRIVER and CLAUDE_ESCALATES_TO to build the model bracket, and CLAUDE_MODE_TAG to prefix the topic:
[Sonnet→Opus] deep · cluster-bootstrap: M10 Gitea CI | 23% ctx
escalates_to: opus→[Sonnet→Opus](shows escalation path)escalates_to: none→[Sonnet](no arrow)- Mode tag is prepended to the topic with
·separator
If active-mode.env is missing or CLAUDE_CONFIG_DIR is unset, the status line falls back to the full model display name from the session JSON with no mode tag.
Dryrun behaviour
When --dryrun (or -n) is passed, claude-profile resolves all values exactly as it would in a real run, then prints them and exits 0 — no claude invocation, no file writes.
$ claude-profile --dryrun --preset deep-cluster
[dryrun] Profile: oreillyit-anthropic (~/.claude-oreillyit-anthropic)
[dryrun] Mode: deep
[dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/deep.md
[dryrun] Driver: sonnet
[dryrun] Model ID: claude-sonnet-4-6
[dryrun] Project: cluster-bootstrap
[dryrun] Async OK: yes
[dryrun] Autoloop: none
[dryrun] Would write: ~/.claude-oreillyit-anthropic/active-mode.env
[dryrun] Would write: ~/.claude-oreillyit-anthropic/last-mode
[dryrun] Would execute: claude --model claude-sonnet-4-6 --append-system-prompt "<context+mode>"
[dryrun] Context length: 4823 chars (3104 from context-load + 1719 from mode body)
[dryrun] No changes made.
Dryrun with MiniMax profile:
$ claude-profile --dryrun oreillyit-minimax --mode quick
[dryrun] Profile: oreillyit-minimax (~/.claude-oreillyit-minimax)
[dryrun] Mode: quick
[dryrun] Mode file: ~/dev/claude/small-scripts/data/claude-profile/modes/quick.md
[dryrun] Driver: sonnet
[dryrun] Model ID: MiniMax-M2.7
[dryrun] Provider env: ~/.claude-oreillyit-minimax/provider.env
[dryrun] Project: <not set, will be picked by CLAUDE.md>
[dryrun] Async OK: no
[dryrun] Autoloop: none
[dryrun] Would execute: claude --model MiniMax-M2.7 --append-system-prompt "<context+mode>"
[dryrun] No changes made.
In dryrun mode the interactive prompts are still shown, so the user can walk through the picker without committing.
Edge cases
| Case | Handling |
|---|---|
~/dev/claude/small-scripts/data/claude-profile/modes/ does not exist |
Error: "Mode files directory not found at ." Exit 1. |
Mode file referenced by --mode does not exist |
Error: "Mode file not found: ." Exit 1. |
| Mode file has malformed frontmatter (missing required field) | Error naming the missing field and the file path. Exit 1. |
provider.env exists but has no MODEL_ID |
Driver default is used; other entries are still exported. |
provider.env specifies ANTHROPIC_API_KEY_FILE but the file does not exist |
Error: "API key file not found: ." Exit 1. |
presets.yaml does not exist and --preset was given |
Error: "Preset '' not found in any presets.yaml." Exit 1. |
presets.yaml exists but the named preset is not in it |
Error: "Preset '' not found in any presets.yaml." Exit 1. |
Preset's profile field references a non-existent profile |
Error. Exit 1. |
--preset and --mode both given |
Error: "mutually exclusive". Exit 1. |
Positional PROFILE and --preset profile field disagree |
Error: "Profile mismatch: positional , preset specifies ." Exit 1. |
last-mode file is missing or contains an unknown mode |
Fall back to quick as the default. |
| Two parallel sessions launched against the same profile | Both write to the same active-mode.env — last writer wins. Documented limitation; not addressed in v1. |
CLAUDE_CONFIG_DIR already set in the environment when claude-profile starts |
Ignored; claude-profile sets it from the resolved profile, overriding whatever was inherited. |
| Mode file body is empty (frontmatter only) | Error: "Mode file has no body — nothing to load into the system prompt." Exit 1. |
Examples
Interactive — full picker
$ claude-profile
Claude Code profiles:
1) default (~/.claude)
2) oreillyit-anthropic (~/.claude-oreillyit-anthropic)
3) oreillyit-minimax (~/.claude-oreillyit-minimax)
Choose profile [1]: 2
Engagement modes:
1) chat — No project, just conversation
2) quick — One small focused job in a known project
3) deep — Hard design or debugging problem
4) hybrid — Haiku driver, Opus on demand
5) orch — Decompose work and dispatch container agents
Choose mode [quick]: 3
Launching: oreillyit-anthropic / deep
[Claude Code starts; the existing CLAUDE.md project picker runs next]
Direct — preset
$ claude-profile --preset deep-cluster
Launching: oreillyit-anthropic / deep / cluster-bootstrap (from preset deep-cluster)
[Claude Code starts]
Direct — profile + mode flag
$ claude-profile oreillyit-anthropic --mode quick --project small-scripts
Launching: oreillyit-anthropic / quick / small-scripts
[Claude Code starts]
Dryrun — verify a preset
$ claude-profile --dryrun --preset deep-cluster
[dryrun] ...
Pass-through to claude
$ claude-profile --mode quick --project small-scripts -- --resume
[passes --resume to claude]
Out of scope (for v1)
- Multi-session coordination (parallel claude-profile launches against the same profile)
- Mid-session mode switching from inside Claude Code (handled by a separate
switch modeskill; requires relaunching) - Editing mode files in place (they are read-only data; customisation happens via
presets.yaml) - Network/remote profiles (everything is local)
- SOPS-encrypted provider credentials at launch time (credentials are stored as plaintext in
~/.claude-secrets/; SOPS decryption is handled by the agent-runtimes container harness for headless agents) - Pinning the model used by named custom subagents at the harness level (those are controlled by the subagent's own definition file; the Agent tool's per-call
modelparameter is unaffected)