# 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-/`. Pass `default` to use `~/.claude/`. | | `CLAUDE_ARGS` | (none) | Arguments after `--` are passed through to `claude` unchanged. | ### Options | Flag | Description | |------|-------------| | `--mode ` | Skip the mode picker and use the named mode (`chat`, `quick`, `deep`, `deep-fable`, `hybrid`, `orch`). | | `--preset ` | Use a named preset from the active profile's `presets.yaml`. Sets profile, mode, and any preset-defined defaults in one shot. | | `--project ` | 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 ` 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-/`. 5. If the directory does not exist, error and exit 1. 6. Export `CLAUDE_CONFIG_DIR=`. 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 ` is given, use it. 3. Else (interactive): list available modes by scanning `/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 `/data/claude-profile/modes/.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` | 2. 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 ` 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 "" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}"`. 10. On exit, the existing WezTerm reset trap runs. ## File contracts ### Mode files — `/data/claude-profile/modes/.md` YAML frontmatter (machine-readable) + markdown body (read by the driver model). Required frontmatter fields: ```yaml name: tag: driver: async_ok: autoloop: plan_mode_auto: spec_driven: escalates_to: # 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: ```yaml 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 "" [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: [dryrun] Async OK: no [dryrun] Autoloop: none [dryrun] Would execute: claude --model MiniMax-M2.7 --append-system-prompt "" [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)