Files
gnoma/docs/profiles.md
T
vikingowl 635dad660c feat(config): per-profile config layering with --profile flag (Phase C-1)
Adds opt-in user profiles for swapping API keys, CLI binaries, and
permission modes between contexts (work/private/experiment/...).

Profile mode engages only when ~/.config/gnoma/profiles/ exists, so
existing single-config installations are untouched. Selection order:
--profile flag → default_profile in base config → fatal error.

Layering: defaults → ~/.config/gnoma/config.toml → profiles/<name>.toml
→ <projectRoot>/.gnoma/config.toml → env. Map sections merge per-key;
[[arms]] and [[mcp_servers]] merge by id/name; [[hooks]] appends.

Per-profile data: quality-<name>.json and sessions/<name>/ keep the
bandit and session list from cross-contaminating between profiles.

Profile names restricted to [A-Za-z0-9_-] to block --profile=../foo
path traversal into derived paths.
2026-05-19 21:35:33 +02:00

5.5 KiB

Profiles

Profiles let you keep multiple independent gnoma configurations and switch between them. Common cases:

  • work vs. private — different API keys, different CLI binaries, stricter or looser permission mode per context.
  • experiment — a non-default SLM model, plan mode, no persistence.

Profile mode is opt-in: gnoma stays on its single-config behavior until you create ~/.config/gnoma/profiles/.

Layout

~/.config/gnoma/
├── config.toml            # base settings + default_profile
├── profiles/              # opt-in directory; presence enables profile mode
│   ├── work.toml
│   ├── private.toml
│   └── experiment.toml
├── quality-work.json      # per-profile router quality data
├── quality-private.json
└── quality-experiment.json

Per-project, session storage segregates the same way:

<projectRoot>/.gnoma/sessions/
├── work/
├── private/
└── experiment/

Loading order

Each gnoma invocation merges configuration in this order (lowest to highest priority):

  1. Built-in defaults.
  2. ~/.config/gnoma/config.toml — the base config.
  3. ~/.config/gnoma/profiles/<name>.toml — the active profile (only when profiles/ exists).
  4. <projectRoot>/.gnoma/config.toml — project overrides.
  5. Environment variables (ANTHROPIC_API_KEY, GNOMA_PROVIDER, etc.).

The active profile is resolved as follows:

  • If --profile <name> is passed on the CLI, that wins.
  • Otherwise, default_profile from the base config.toml is used.
  • If neither is set and profiles/ exists, gnoma fails fast with a list of available profiles. (Silent fallback to defaults would hide configuration mistakes.)

Example: base + two profiles

~/.config/gnoma/config.toml:

default_profile = "work"

# Settings here apply to every profile unless the profile overrides them.
[tools]
bash_timeout = "30s"

~/.config/gnoma/profiles/work.toml:

[provider]
default = "anthropic"
[provider.api_keys]
anthropic = "${ANTHROPIC_WORK_KEY}"

[cli_agents]
claude = "claude-work"

[permission]
mode = "default"

[slm]
backend = "ollama"
model = "reecdev/tiny3.5:1.5b"

~/.config/gnoma/profiles/private.toml:

[provider]
default = "openai"
[provider.api_keys]
openai = "${OPENAI_PRIVATE_KEY}"

[cli_agents]
claude = "claude-priv"

[permission]
mode = "auto"

[slm]
backend = "ollama"
model = "reecdev/tiny3.5:500m"

~/.config/gnoma/profiles/experiment.toml:

[provider]
default = "mistral"
model = "mistral-large-latest"

[permission]
mode = "plan"

[slm]
enabled = false        # turn the classifier off entirely

[session]
max_keep = 0           # don't keep session history for experiments

Switching

gnoma --profile work providers       # use work profile
gnoma --profile private              # private profile, default subcommand (TUI)
gnoma                                # base default_profile (here: work)

Profile selection is per-invocation. Restart re-reads default_profile; no "last used" persistence — explicit switches stay explicit.

Merge semantics

  • Scalars (provider.default, provider.model, tools.bash_timeout, …): the profile value wins if set; otherwise base is preserved.
  • Maps (provider.api_keys, provider.endpoints, cli_agents, rate_limits): per-key merge. Profile overrides individual keys without erasing the rest.
  • [[hooks]]: profile hooks are appended after base hooks.
  • [[arms]]: merged by id. Profile entries override the matching base entry; new IDs append. So a profile can tweak one arm's cost_weight without redeclaring the rest.
  • [[mcp_servers]]: merged by name (same policy as arms).
  • [security], [plugins], etc.: profile replaces if the profile defines anything in that section.

The project-level .gnoma/config.toml layer applies on top of the merged base+profile result. Environment variables apply last and override everything.

Profile name rules

Names must match [A-Za-z0-9_-]+. Dots, slashes, spaces, and other characters are rejected to keep derived paths (quality-<name>.json, sessions/<name>/) predictable and to prevent path traversal via --profile.

Where per-profile data lives

Data Path
Router quality (bandit telemetry) ~/.config/gnoma/quality-<profile>.json
Session history <projectRoot>/.gnoma/sessions/<profile>/
Plugins ~/.config/gnoma/plugins/ (shared across profiles)
Skills ~/.config/gnoma/skills/ (shared across profiles)

Plugins and skills stay global on purpose — they're code, not preferences. Use profile-specific [plugins].enabled / disabled lists if you need a different mix per profile.

gnoma router stats and profiles

When a profile is active, gnoma router stats reads quality-<profile>.json and prefixes its output with the profile name so it's clear which dataset you're looking at. To compare profiles:

gnoma --profile work    router stats
gnoma --profile private router stats

Backward compatibility

If ~/.config/gnoma/profiles/ does not exist, gnoma behaves exactly as before:

  • Reads ~/.config/gnoma/config.toml as the only base config.
  • Stores quality data at ~/.config/gnoma/quality.json.
  • Stores sessions at <projectRoot>/.gnoma/sessions/ (no profile subdirectory).
  • --profile <name> returns a clear error pointing you at the profiles/ directory to create.

Existing single-config installations don't need to do anything.