Files
small-scripts/specs/claude-profile.spec.md
Paul O'Reilly 143b32597e Add claude-profile engagement mode picker with statusline and session-start integration
- claude-profile: phase 1-3 picker (profile, mode, launch) with preset support,
  dryrun, WezTerm theming, and --append-system-prompt mode body injection
- 5 mode files (chat/quick/deep/hybrid/orch) with YAML frontmatter + prose body;
  new escalates_to field drives statusline →Opus arrow for deep and hybrid
- statusline.sh reads CLAUDE_CONFIG_DIR/active-mode.env to show
  [Sonnet→Opus] deep · topic format when launched via claude-profile
- Root CLAUDE.md session-start: auto-selects project from cwd or CLAUDE_PROJECT
  in active-mode.env, skipping the interactive picker when context is clear
- Spec, tests (37 assertions, 9 test files, all passing), context docs, and
  preset example included

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 00:36:38 +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; the engagement mode bundles a driver model, subagent policy, async tolerance, and workflow stance into a preset that the driver model adopts at session start.

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

  • --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, time horizon, async, autoloop). Skip the rest of phase 2.
  2. Else if --mode <name> is given, use it and proceed to mode-specific questions (project, async, etc., depending on the mode).
  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. After the mode is chosen, ask only the questions relevant to that mode (see "Mode follow-up questions" below). Skip questions already answered by CLI flags.
  5. Validate the chosen mode file exists at <repo>/data/claude-profile/modes/<name>.md. If not, error and exit 1.
  6. Read the mode file's frontmatter to extract driver, tag, async_ok, autoloop, plan_mode_auto, spec_driven.

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. Resolve the mode's driver frontmatter field through the Driver mapping table below to a concrete model ID.
  6. Execute claude --model "<resolved-model-id>" --append-system-prompt "$context_plus_mode" "${CLAUDE_ARGS[@]}".
  7. On exit, the existing WezTerm reset trap runs.

Driver mapping

The driver field in mode frontmatter uses a logical name; the launcher maps it to the full Claude model ID before passing to claude --model. Full IDs are used (not aliases) to remove ambiguity about which model version is selected.

Frontmatter driver --model argument
haiku claude-haiku-4-5-20251001
sonnet claude-sonnet-4-6
opus claude-opus-4-6
opus-1m claude-opus-4-6[1m]

If the frontmatter specifies an unknown driver, the launcher errors with the list of valid values and exits 1. The mapping table lives in the launcher script as a constant; updating it is the only change needed when new model versions are released.

Mode follow-up questions

Each mode declares which questions are relevant. Asked in order; only those not already supplied via CLI flags are prompted for.

Question Asked in modes Default Drives
Time horizon? (minutes / hours / overnight) deep, hybrid, orch hours Recorded in active-mode.env; the model adapts ScheduleWakeup cadence
Driver override? (force a specific Claude model) quick, deep mode default Overrides the driver field; resolved through the Driver mapping table and passed to claude --model
Async OK? (yes / no) deep, hybrid mode default (yes for both) Overrides async_ok
Auto-loops? (yes / no, confirms /loop 2m /orchestrate) orch yes Overrides autoloop

chat and quick ask no questions interactively.

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. A future CLAUDE.md update will read that field and skip its own picker when set; until then, supplying --project will result in the CLAUDE.md picker still appearing — the user can confirm the same project. This is a documented v1 limitation.

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.

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_TIME_HORIZON=hours
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.

Last project per mode — $CLAUDE_CONFIG_DIR/last-mode-project-<mode>

Single line: the project name last used with this mode. Per-mode so switching modes doesn't lose the previous mode's project context.

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
  mode: deep
  project: cluster-bootstrap
  time_horizon: hours
  async: yes

quick-skills:
  profile: oreillyit
  mode: quick
  project: custom-claude-skills

orch-overnight:
  profile: oreillyit
  mode: orch
  project: ai-image-gen
  time_horizon: overnight
  autoloop: yes

The profile field is required. Other fields default to the mode's defaults if omitted.

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 (~/.claude-oreillyit)
[dryrun] Mode:            deep (Sonnet driver, Opus via named workflows)
[dryrun] Mode file:       ~/dev/claude/small-scripts/data/claude-profile/modes/deep.md
[dryrun] Project:         cluster-bootstrap
[dryrun] Time horizon:    hours
[dryrun] Async OK:        yes
[dryrun] Autoloop:        none
[dryrun] Would write:     ~/.claude-oreillyit/active-mode.env
[dryrun] Would write:     ~/.claude-oreillyit/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 file)
[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 . Reinstall claude-profile or check the repo location." Exit 1.
Mode file referenced by --mode does not exist Error listing the available modes. Exit 1.
Mode file has malformed frontmatter (missing required field) Error naming the missing field and the file path. Exit 1.
presets.yaml does not exist and --preset was given Error: "No presets.yaml found at ." Exit 1.
presets.yaml exists but the named preset is not in it Error listing available preset names. Exit 1.
Preset's profile field references a non-existent profile Error. Exit 1.
--preset and --mode both given Error: "Mutually exclusive: --preset already specifies a mode." 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 (~/.claude-oreillyit)
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

Time horizon? [hours]: 
Async OK? [yes]: 

Launching: oreillyit / deep
[Claude Code starts; the existing CLAUDE.md project picker runs next]

Direct — preset

$ claude-profile --preset deep-cluster
Launching: oreillyit / deep / cluster-bootstrap (from preset deep-cluster)
[Claude Code starts]

Direct — profile + mode flag

$ claude-profile oreillyit --mode quick --project small-scripts
Launching: oreillyit / 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, not claude-profile itself)
  • Editing mode files in place (mode files are read-only data; customisation happens via presets.yaml)
  • Network/remote profiles (everything is local)
  • Mid-session driver model swaps (the harness pins the model at launch via --model; switching mid-session requires relaunching)
  • Pinning the model used by named custom subagents at the harness level (those are controlled by the subagent's own definition file under subagents[name].model; the Agent tool's per-call model parameter is unaffected and remains how mode prose instructs the driver to escalate)