Skip to content

Global agent identity & hermes-style profiles (unify on ~/.lich/**) #117

Description

@Moikapy

Summary

Lich's persona (agent name, theme, system prompt, provider/model) lives only in per-project .lich/config.json. Every new project re-runs the setup wizard and re-answers "who is this agent?". We want a global default identity — and, as a follow-on, hermes-style named profiles — stored under ~/.lich/**, unifying with the existing global themes directory (~/.lich/themes/, see src/util/theme.ts).

Maintainer decision (2026-09-23): the global layer holds a full config (provider, model, max_turns, gateway, identity keys); the project .lich/config.json overrides the global base per key.

Research: how hermes does it

(From hermes-agent's official docs, website/docs/user-guide/profiles.md, features/personality.md, guides/use-soul-with-hermes.md, read 2026-09-23.)

  • Profiles = separate home directories (~/.hermes/profiles/<name>/; the default profile is ~/.hermes itself), selected by setting HERMES_HOME. -p <name> targets one per run; profile use <name> sets a sticky default ("like kubectl config use-context"); each profile auto-gets a command alias (coder chat). A directory only counts as a profile when it carries an identity file (config.yaml/.env/SOUL.md/profile.yaml/auth.json/state.db) — stray dirs are ignored.
  • Persona = SOUL.md, loaded only from the profile home, never the working directory — stated rationale: "the personality belongs to the Hermes instance itself", so it cannot change between projects. Injected verbatim as slot feat: lich update subcommand (self-update wrapper) #1 of the system prompt, fully replacing the built-in identity; auto-seeded starter; never overwritten; missing/empty → built-in default identity; capped (20k chars) after a security scan; edits take effect on the next session (frozen snapshot).
  • The display.personality config key is documented as legacy cosmetic; the live /personality TUI overlay is a separate, transient layer.
  • Profiles do not sandbox the agent; tool subprocesses keep the real HOME (opt-in terminal.home_mode: profile for strict isolation).
  • Clones never copy messaging bot tokens (two gateways on one token collide); OAuth logins are shared, not copied.
  • Lich avoids hermes' "never point two agents at one profile" hazard by construction: lich freezes config at parse time (src/agent/config.ts) and keeps sessions/plugins in the project, so a lich profile is naturally identity+model while per-project state stays put. Lich also stores only env-var names for tokens (token_envs), so profile copies cannot leak secrets.

Design

Target layout:

~/.lich/
├── config.json      # global default config (full AgentConfig)
├── themes/          # already global today
└── profiles/        # hermes-style named profiles
    ├── <name>.json  # full AgentConfig for the profile
    └── <name>.md    # optional "soul": injected into system_prompt

Config resolution (top wins on key conflicts):

  1. --config <path> — explicit file, replaces everything (current behavior, unchanged)
  2. --profile <name> — ~/.lich/profiles/<name>.json as base layer (+ optional <name>.md → system_prompt)
  3. Project <work_dir>/.lich/config.json — merged over the base layer
  4. Global ~/.lich/config.json — base layer when no profile is selected
  5. LICH_MODEL/env fallback — current env_provider path, unchanged

Merge semantics (v1, deliberately simple): shallow merge, project-wins per key; when the project config has a providers array it replaces the base's array (no per-provider merge).

Implementation plan

Phase 0 — unify the global home on ~/.lich/**

  • src/cli_config.ts: change DEFAULT_USER_CONFIG (currently .config/lich/config.json) to ~/.lich/config.json; keep the old ~/.config/lich/config.json as a trailing fallback entry for back-compat (read-only, never written)
  • Update tests asserting the chain: test/cli_config.test.ts (config_search_paths cases), test/first_run.test.ts
  • Update docs stating the fallback: docs/user-guide/cli.md (wizard + config-search sections), docs/getting-started.md

Phase 1 — global default config (merge)

  • Change resolution from "first found file wins" to project-merged-over-global; --config stays a full replacement
  • Wizard: when a global config exists, prefill answers from it; add a "save as global default?" step (writes ~/.lich/config.json)
  • Add lich init --global (or equivalent) to write the global file without the wizard
  • Tests: merge precedence, wizard prefill, global init
  • Docs: config reference in docs/user-guide/cli.md, docs/getting-started.md

Phase 2 — hermes-style profiles

  • ~/.lich/profiles/<name>.json + optional <name>.md soul (missing/empty → no injection; project system_prompt wins over the soul)
  • New src/cli_profile.ts: lich profile list|show|create|use — create runs the wizard pointed at the global dir; use records the sticky default profile
  • --profile <name> flag + LICH_PROFILE env var, wired in src/cli.ts
  • Sticky-default resolution: --profile > project config > sticky default > global config > env
  • Guard check: with work_dir = home directory, file tools must not read/write ~/.lich/config.json or ~/.lich/profiles/** — verify assert_file_tool_access (src/tools/guard.ts) covers this (existing .lich rules deny config.json and limit writes to skills/+plugins/); extend if needed + regression test
  • Tests: profile resolution precedence, profile subcommands, guard regression
  • Docs: new docs/user-guide/profiles.md (model it on hermes' profiles.md structure), CHANGELOG staging

Open decisions

  • Keep ~/.config/lich/config.json as a trailing fallback, or hard-cut? (leaning: keep, read-only)
  • Sticky default profile stored as a key inside ~/.lich/config.json, or a separate marker file? (leaning: key inside config.json)
  • Show the active profile name in the TUI banner (hermes shows Profile: coder)? (separate small follow-up if wanted)

Acceptance criteria

  • lich in a fresh project with a global config present no longer re-asks name/model (wizard prefilled or skipped)
  • ~/.lich/ is the only global location lich writes; nothing writes to ~/.config/lich/ anymore
  • Project config still fully overrides the global base per key
  • lich --profile <name> "task" resolves config from the profile with the project config merged on top
  • File tools cannot read or write global identity files even when work_dir is the home directory

Activity

  1. Moikapy commented on Oct 5, 2026

    @Moikapy
    OwnerAuthor

    Done in three PRs:

    All acceptance criteria are met. Showing the active profile in the TUI banner was optional and is not done; it can be a small follow-up.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions