You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Global agent identity & hermes-style profiles (unify on ~/.lich/**) #117
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
--profile <name> — ~/.lich/profiles/<name>.json as base layer (+ optional <name>.md → system_prompt)
Project <work_dir>/.lich/config.json — merged over the base layer
Global ~/.lich/config.json — base layer when no profile is selected
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
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
feat(config): named profiles in ~/.lich/profiles #172: named profiles (~/.lich/profiles/<name>.json + optional <name>.md soul), --profile / LICH_PROFILE / lich profile list|show|create|use, and the file-tool guard for .lich/profiles.
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.
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/, seesrc/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.jsonoverrides 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.)~/.hermes/profiles/<name>/; the default profile is~/.hermesitself), selected by settingHERMES_HOME.-p <name>targets one per run;profile use <name>sets a sticky default ("likekubectl 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.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).display.personalityconfig key is documented as legacy cosmetic; the live/personalityTUI overlay is a separate, transient layer.HOME(opt-interminal.home_mode: profilefor strict isolation).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:
Config resolution (top wins on key conflicts):
--config <path>— explicit file, replaces everything (current behavior, unchanged)--profile <name>—~/.lich/profiles/<name>.jsonas base layer (+ optional<name>.md→system_prompt)<work_dir>/.lich/config.json— merged over the base layer~/.lich/config.json— base layer when no profile is selectedLICH_MODEL/env fallback — currentenv_providerpath, unchangedMerge semantics (v1, deliberately simple): shallow merge, project-wins per key; when the project config has a
providersarray it replaces the base's array (no per-provider merge).Implementation plan
Phase 0 — unify the global home on
~/.lich/**src/cli_config.ts: changeDEFAULT_USER_CONFIG(currently.config/lich/config.json) to~/.lich/config.json; keep the old~/.config/lich/config.jsonas a trailing fallback entry for back-compat (read-only, never written)test/cli_config.test.ts(config_search_paths cases),test/first_run.test.tsdocs/user-guide/cli.md(wizard + config-search sections),docs/getting-started.mdPhase 1 — global default config (merge)
--configstays a full replacement~/.lich/config.json)lich init --global(or equivalent) to write the global file without the wizarddocs/user-guide/cli.md,docs/getting-started.mdPhase 2 — hermes-style profiles
~/.lich/profiles/<name>.json+ optional<name>.mdsoul (missing/empty → no injection; projectsystem_promptwins over the soul)src/cli_profile.ts:lich profile list|show|create|use— create runs the wizard pointed at the global dir;userecords the sticky default profile--profile <name>flag +LICH_PROFILEenv var, wired insrc/cli.ts--profile> project config > sticky default > global config > envwork_dir= home directory, file tools must not read/write~/.lich/config.jsonor~/.lich/profiles/**— verifyassert_file_tool_access(src/tools/guard.ts) covers this (existing.lichrules denyconfig.jsonand limit writes toskills/+plugins/); extend if needed + regression testdocs/user-guide/profiles.md(model it on hermes' profiles.md structure), CHANGELOG stagingOpen decisions
~/.config/lich/config.jsonas a trailing fallback, or hard-cut? (leaning: keep, read-only)~/.lich/config.json, or a separate marker file? (leaning: key inside config.json)Profile: coder)? (separate small follow-up if wanted)Acceptance criteria
lichin 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/anymorelich --profile <name> "task"resolves config from the profile with the project config merged on topwork_diris the home directory