Files
small-scripts/specs/claude-profile.spec.md
Paul O'Reilly 16586541ea claude-profile: bump opus to 4.8, add fable driver
- driver_model_id(): opus -> claude-opus-4-8, opus-1m -> claude-opus-4-8[1m]
- driver_model_id(): add fable -> claude-fable-5
- error message updated: Valid: haiku sonnet opus opus-1m fable
- spec: driver->model table updated (opus 4.8, fable added); frontmatter
  driver enum includes fable; note that opus-1m retained for [1m] convention
- tests: add Test 13 (deep-fable dryrun asserting claude-fable-5);
  add Test 14 (unknown driver exits 1) — 47 assertions total
  Note: deep-fable mode file lands in the next commit
2026-06-12 21:33:58 +12:00

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, deep-fable, 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

  • --preset is mutually exclusive with --mode and --project — the preset defines all of those. Passing both is an error.
  • A positional PROFILE combined with --preset is allowed only if the preset's profile field 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

  1. If --preset <name> is given, read the preset from the global presets file (see "File contracts" below) and use its profile field. Skip the rest of phase 1.
  2. Else if PROFILE is given as a positional argument, use it directly.
  3. Else (interactive): list profiles by scanning ~/.claude-* directories, display a numbered menu, prompt for selection. Default selection is default (~/.claude/).
  4. Resolve the profile to a directory: default~/.claude/; otherwise ~/.claude-<NAME>/.
  5. If the directory does not exist, error and exit 1.
  6. Export CLAUDE_CONFIG_DIR=<resolved>.
  7. Emit the WezTerm theme escape sequence for the profile (existing behaviour, unchanged).

Phase 2 — Mode selection

  1. If --preset is given, use the preset's mode and any other fields (project, async, autoloop). Skip the rest of phase 2.
  2. Else if --mode <name> is given, use it.
  3. Else (interactive): list available modes by scanning <repo>/data/claude-profile/modes/*.md, display a numbered menu with each mode's tag and the first sentence of its ## Purpose section. Default selection is the contents of $CLAUDE_CONFIG_DIR/last-mode (if present), else quick.
  4. Validate the chosen mode file exists at <repo>/data/claude-profile/modes/<name>.md. If not, error and exit 1.
  5. 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:

  1. 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-8
opus-1m claude-opus-4-8[1m] (Opus 4.8 has 1M context natively; opus-1m is retained for the [1m] launch convention)
fable claude-fable-5
  1. If $CLAUDE_CONFIG_DIR/provider.env exists, parse it. If it contains a MODEL_ID entry, 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

  1. Write the resolved values to $CLAUDE_CONFIG_DIR/active-mode.env (key=value, one per line, see "File contracts").
  2. Write the chosen mode name to $CLAUDE_CONFIG_DIR/last-mode.
  3. Read the mode file's prose body (everything after the frontmatter) and append it to the context that context-load produces. The mode body becomes part of --append-system-prompt.
  4. Set CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1 (existing behaviour).
  5. Set COLORTERM=truecolor for proper colour support.
  6. If provider.env specifies ANTHROPIC_BASE_URL, export it.
  7. If provider.env specifies ANTHROPIC_API_KEY_FILE, read the key from the file and export it as ANTHROPIC_API_KEY. Error if the file is missing.
  8. Export any other entries from provider.env as environment variables.
  9. Execute claude --model "<model_id>" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}".
  10. 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|fable>
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 to claude --model
  • ANTHROPIC_BASE_URL — exported as-is
  • ANTHROPIC_API_KEY_FILE — file is read and its contents exported as ANTHROPIC_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 mode skill; 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 model parameter is unaffected)