Files

464 lines
43 KiB
Markdown
Raw Permalink Normal View History

---
name: hermes-profile-management
description: "Create, configure, and validate Hermes profiles with linked workspaces. Covers the full lifecycle: clone, model selection, cwd binding, AGENTS.md seeding, and two-round validation."
version: 1.3.0
author: agent
license: MIT
platforms: [linux]
metadata:
hermes:
tags: [hermes, profiles, workspace, configuration, setup]
related_skills: [hermes-agent, hermes-config-bulk-update]
---
# Hermes Profile Management
Create and configure Hermes profiles with linked workspace directories. This skill covers the standard workflow for spinning up a new profile, binding it to a workspace, and validating everything is correct.
## Trigger Conditions
- User asks to create a new Hermes profile
- User asks to set up a workspace for a specific purpose (VS Code, coding, research, etc.)
- User asks to clone an existing profile for a new use case
- User says they already cloned/copied a profile and asks to "finish setup" or "update files" — see Audit Mode below
## Prerequisites
- Hermes Agent installed and configured
- At least one existing profile to clone from (usually `general`)
- Workspace directory convention: `/home/n8n/workspace/<profile-name>/`
## Workflow
### 1. Survey Current State
Before creating anything, check what exists:
```bash
hermes profile list # See all profiles, models, gateway status
ls -la /home/n8n/workspace/ # See existing workspace directories
```
### 2. Confirm Model Choice
Ask the user which model to use. Present the most common options from existing profiles. Default to `deepseek-v4-pro:cloud` if the user doesn't specify — it's the most common choice across profiles.
### 3. Create Workspace Directory
```bash
mkdir -p /home/n8n/workspace/<profile-name>
```
### 4. Create Profile (Clone from Source)
```bash
hermes profile create <name> --clone-from <source-profile>
```
This copies config.yaml, .env, SOUL.md, and skills from the source.
### 5. Bind Workspace to Profile
Set BOTH keys — they are independent and both required:
```bash
hermes config set terminal.cwd /home/n8n/workspace/<name> --profile <name>
hermes config set workspace /home/n8n/workspace/<name> --profile <name>
```
`terminal.cwd` controls where shell commands start. `workspace:` controls which workspace root Hermes loads AGENTS.md / CLAUDE.md / .cursorrules from. A profile with only one set will have subtle failures (wrong context files loaded, or commands running from home directory).
### 6. Create AGENTS.md
Write `/home/n8n/workspace/<name>/AGENTS.md` with:
- Profile name and purpose
- Model and provider
- Usage notes
- Any profile-specific conventions
### 7. Validate (Two Rounds)
**Round 1 — Structural:**
```bash
hermes profile show <name> # Profile exists, model correct
ls -la /home/n8n/workspace/<name>/ # Directory exists, AGENTS.md present
grep "cwd:" ~/.hermes/profiles/<name>/config.yaml # cwd points to workspace
grep -A3 "^model:" ~/.hermes/profiles/<name>/config.yaml | head -4 # Model correct
```
**Round 2 — Functional:**
```bash
hermes -p <name> config check # Config passes health check
touch /home/n8n/workspace/<name>/.test_write && rm /home/n8n/workspace/<name>/.test_write # Writable
hermes profile list | grep <name> # Appears in profile list
hermes -p <name> config # Full config dump confirms settings
```
## Audit Mode (Partially-Completed Clones)
When the user says they already cloned a profile or copied a workspace but asks you to "finish setup" or "update files," do NOT assume the standard workflow steps are done. Audit each layer before fixing:
1. **Workspace dir**: `ls -la /home/n8n/workspace/<name>/` — may not exist even if the profile does. Create + copy from source if missing.
2. **cwd binding**: `grep "cwd:" ~/.hermes/profiles/<name>/config.yaml` — often still `.` or unset. Bind it: `hermes config set terminal.cwd /home/n8n/workspace/<name> --profile <name>`
3. **AGENTS.md**: Check if it still references the source profile name/path. Rewrite for the new workspace (workspace path, purpose, model, provider, hindsight note).
4. **Cloned cron jobs**: `cat ~/.hermes/profiles/<name>/cron/jobs.json` — the clone inherits ALL of the source's cron jobs. Any job that writes to a shared resource (Qdrant collection, database, file path) will now run TWICE — once from each profile. Pause or delete duplicates in the new profile. This is the most commonly missed step.
5. **Hindsight bank_id (LEGACY — migrated to Holographic 2026-07-01)**: `cat ~/.hermes/profiles/<name>/hindsight/config.json` — the clone inherits the source's `bank_id` (e.g., `hermes`). Memories are SHARED across both profiles, not isolated. This is usually fine for dev/test profiles but document it in AGENTS.md so the operator knows. If isolation is needed, change `bank_id` to a unique value.
6. **Config version**: `hermes -p <name> config check` — the clone may be behind the source's config version. Run `hermes -p <name> config migrate` if so.
## Profile Deletion
When the user asks to remove profiles, follow this workflow:
### 1. List and Number
Present all profiles as a numbered list so the user can pick by number. Use `hermes profile list` and number them sequentially.
### 2. Confirm Workspaces
Before deleting, check which profiles have matching workspace directories:
```bash
for p in <profile_names>; do ls -d ~/workspace/$p 2>/dev/null && echo "exists" || echo "no workspace"; done
```
### 3. Delete Workspaces First
Workspaces are just directories — remove them directly:
```bash
rm -rf ~/workspace/<name>
```
### 4. Delete Profiles (Interactive Confirmation)
`hermes profile delete` requires interactive confirmation — you must type the profile name. Pipe it in:
```bash
echo "<name>" | hermes profile delete <name>
```
There is no `--force` flag. The confirmation prompt is mandatory.
### 5. Verify
```bash
hermes profile list # Profile gone
ls ~/workspace/<name> 2>&1 # Workspace gone (No such file or directory)
```
### Pitfalls
- **`--force` does not exist**: `hermes profile delete` has no `--force` flag. The only way to automate is piping the name to stdin.
- **Delete workspaces before profiles**: Workspaces are simple `rm -rf`. Profiles require interactive confirmation. Do workspaces first so if the profile delete fails, you haven't lost the workspace yet.
- **Never delete the active profile**: The current profile (marked with ◆) cannot be deleted while in use. Switch away first if needed.
## Pitfalls
- **User mentions a name → check local state BEFORE web search**: When the user says a name you don't recognize (e.g., "openz"), do NOT immediately search the web for it. Check local state first: `hermes profile list` (could be a profile), `hermes skills list` (could be a skill), memory/fact_store (could be a known entity), `session_search` (could be from a prior session). Only search the web if all local checks come up empty. The user will be frustrated if you treat their local resources as unknown internet things.
- **Check memory/fact_store for infrastructure state before assuming something isn't installed**: When the user asks you to set up or integrate a service (ComfyUI, Open WebUI, etc.), check memory and fact_store for existing infrastructure BEFORE writing a plan that assumes it needs to be installed from scratch. The user may have set it up in a prior session. If the user says "check memory," you've already made the mistake — stop, check, and revise the plan. This is the infrastructure variant of the name-resolution pitfall above.
- **Scope incrementally — start with the simplest working path, defer advanced features**: When the user says "first I just want X for now, more later," write the plan for X only. Explicitly list deferred features in a "Future Phases" section so the user sees you heard them, but do NOT include those phases in the current scope. The user wants a working deliverable fast, not a maximal architecture document. This applies to integration plans, feature rollouts, and any multi-phase work.
- **Backend API server profiles must declare their interface in AGENTS.md**: When a Hermes profile serves as a backend for a web UI (Open WebUI, custom frontend, etc.), its AGENTS.md MUST include a prominent "Your Main Interface is X" section at the top. This section must explicitly state: (a) the agent is a backend API server, not the UI, (b) users interact through the web interface, not directly, (c) UI features like image generation, TTS, STT, and vision are handled by the web UI — do NOT try to enable those Hermes toolsets, (d) the agent's tools (terminal, file, web, memory) are for backend tasks only. Without this section, the agent will try to enable `image_gen`, `tts`, `vision`, and `browser` toolsets to fulfill user requests, wasting resources and confusing the model. See `references/open-webui-backend-profile-pattern.md` for the canonical template.
- **AGENTS.md alone won't fix a running session — use agent.system_prompt too**: The agent loads AGENTS.md at session start. If the user is in a session that started before the AGENTS.md update, the agent will still exhibit the old behavior. Always pair AGENTS.md updates with `hermes -p <profile> config set agent.system_prompt "..."` to inject the directive into every session immediately. `agent.system_prompt` appends to the system prompt after the identity block and takes effect without a session restart. Apply to ALL backend profiles at once with a `for` loop. See `references/open-webui-backend-profile-pattern.md` for the full dual-delivery pattern.
- **model.base_url MUST match model.default (critical, silent failure)**: The #1 silent error in cloned configs. `model.default` names a model, `model.base_url` points at the endpoint that serves it. If the source profile's `base_url` was an Epyc vLLM endpoint (10.0.0.26:8000, serves only `qwen3.6-35b-a3b-uncensored`) and the new profile's `model.default` is `minimax-m3:cloud` (served by Ollama at localhost:11434), the primary model will silently fail to load. ALWAYS cross-check: if model.default is a `:cloud` Ollama-routed model, base_url must be `http://localhost:11434/v1`, NOT an Epyc/DGX endpoint. The fix: `config['model']['base_url'] = 'http://localhost:11434/v1'` for any Ollama-routed primary model.
- **context_length: 131072, not 262144**: DGX qwen3 DFlash has a native max context of 262,144 tokens, but you MUST configure `model.context_length: 131072`. The DGX vLLM runs `max_num_seqs: 3` with a shared ~457K token KV pool. Setting 262144 limits you to 1 concurrent stream; 131072 allows 3 streams. Epyc qwen3.6-35b uses 262144 (single stream, no KV pool sharing).
- **browser.allow_private_urls must match security.allow_private_urls**: If `security.allow_private_urls: true` but `browser.allow_private_urls: false`, the browser tool will silently block LAN URLs (10.0.0.x) even though security permits them. ALWAYS set both to the same value.
- **display.platforms leftovers after section removal**: Removing the top-level `telegram:` and `discord:` sections does NOT remove `display.platforms.telegram` and `display.platforms.discord`. After removing platform sections, explicitly clean `display.platforms` (set to `{}` for non-gateway profiles).
- **Clone inherits base_url**: The cloned profile gets the source's `model.base_url`. If the source uses a remote endpoint (e.g., `http://10.0.0.26:8000/v1`), the new profile will too. This is usually correct but verify if the profile needs a different backend.
- **Clone inherits cron jobs**: The clone gets a COPY of the source's `cron/jobs.json`. Any job that targets shared infrastructure (Qdrant, databases, file paths) will execute from both profiles on the same schedule. Always audit and pause/delete duplicates in the new profile. This is the #1 missed step in partial-clone setups.
- **Clone inherits hindsight bank_id (LEGACY — migrated to Holographic 2026-07-01)**: Memories are shared across profiles with the same `bank_id`. NOT isolated by default. For all profiles except `general`, change `bank_id` in `hindsight/config.json` to `hermes-<profile>` (e.g. `hermes-dev`, `hermes-finance`). Only `general` keeps `bank_id: hermes` (shared, primary). **The `hindsight/` directory may not be copied at all during clone** — if `~/.hermes/profiles/<name>/hindsight/config.json` doesn't exist, create the directory and write config.json from scratch (mirror the source profile's config.json but change `bank_id`). Without a profile-level config.json, the profile falls back to the base `~/.hermes/hindsight/config.json` which has `bank_id: hermes` — the cross-profile bank_id leak pitfall.
- **Clone inherits config version**: May be behind the source. Run `hermes config migrate` to bring it current.
- **AGENTS.md still references source workspace**: Copied workspace files still have the old workspace path and purpose. Rewrite AGENTS.md for the new profile.
- **Profile alias**: `hermes profile create` auto-creates a wrapper at `~/.local/bin/<name>`. Use it as `vscode chat` or `hermes -p vscode`.
- **`~/.hermes/active_profile` is the sticky-default file**: `hermes profile use <name>` writes the profile name to `~/.hermes/active_profile`. When `hermes` runs without `-p`, it reads this file to determine which profile to load. If bare `hermes` loads the wrong profile, check this file first — it may have been set by a prior `hermes profile use` or manual edit. The file contains a single line with the profile name. Fix: `echo "general" > ~/.hermes/active_profile` or `hermes profile use general`.
- **Profile alias preferred over `-p` flag for dispatcher skills**: Some models (notably minimax-m3:cloud) hallucinate that `hermes -p` doesn't exist, even though it's a valid global flag. When writing dispatcher skills that delegate to another profile, use the profile alias (`research -s ...`) instead of `hermes -p research -s ...`. The alias is at `~/.local/bin/<name>` and works from any directory. This avoids the model second-guessing the command and falling back to doing the work itself.
- **Skills are cloned**: The new profile gets a copy of the source's skills directory. They diverge independently after creation.
- **Fallback model should be DGX qwen, not Ollama**: The standard fallback is `qwen` via DGX (http://10.0.0.6:8000/v1, api_key: dummy), NOT another Ollama model. DGX is the failover endpoint — if Ollama is down, the fallback must point at a different infrastructure, not the same one.
- **cronjob tool pause may not persist to jobs.json**: The `cronjob action='pause'` tool reports success and returns `enabled: false, state: paused` in its response, but the on-disk `cron/jobs.json` may still show `enabled: true, state: scheduled` on next read. The tool's in-memory state and the file can diverge. After pausing, ALWAYS verify by reading jobs.json directly, and if it didn't persist, write the pause directly: `j['enabled']=False; j['state']='paused'; j['paused_at']=<iso>; j['paused_reason']=<reason>` and dump back to the file. This is the only reliable way to pause a cloned cron job.
- **.env HINDSIGHT_LLM_* may point to wrong endpoint after clone**: The cloned .env may have `HINDSIGHT_LLM_BASE_URL=http://localhost:11434/v1` (Ollama) and `HINDSIGHT_LLM_MODEL=qwen3:8b` while hindsight/config.json correctly points to Epyc (`http://10.0.0.26:8000/v1`, `qwen3.6-35b-a3b-uncensored`). The .env overrides config.json at runtime. ALWAYS check .env's HINDSIGHT_LLM_MODEL and HINDSIGHT_LLM_BASE_URL match config.json — the Hindsight LLM does extraction/synthesis and must use the strongest reasoning model (Epyc), not local Ollama.
- **Hindsight bank-level config is missing on cloned/new banks**: When a new bank_id is first used (e.g. `hermes-dev`), Hindsight auto-creates it with ALL defaults (concise extraction, no memory defense, 2048 recall tokens, no disposition, no missions). You MUST apply the bank-level config via the daemon API after the bank exists. See `references/hindsight-bank-config-apply.md` for the API path, payload format, and replication script.
- **The `patch` tool refuses writes to config.yaml**: You cannot use the `patch` tool on `~/.hermes/profiles/<name>/config.yaml` — it errors with "Agent cannot modify security-sensitive configuration." Use `hermes config set <key> <value>` instead, which writes safely and confirms the change. For the top-level `workspace:` key: `hermes config set workspace /workspace/<name>`. For `terminal.cwd`: `hermes config set terminal.cwd /home/n8n/workspace/<name>`. Both keys exist independently — `workspace:` controls which workspace's AGENTS.md gets auto-loaded, `terminal.cwd` controls the shell's starting directory. When binding a profile to its workspace, set BOTH.
- **`workspace:` key vs `terminal.cwd` are independent**: The top-level `workspace:` key (e.g. `/workspace/general`) controls which workspace root Hermes loads AGENTS.md / CLAUDE.md / .cursorrules from. The `terminal.cwd` key controls where shell commands start. They can diverge after a clone or partial setup — always verify both point at the intended workspace for the active profile.
- **Symptom: model lists files from home directory instead of workspace**: When `terminal.cwd: .` (the default), the session starts in whatever directory the process was launched from — typically `/home/n8n`. If the user says "list md files in your workspace" and the model returns files from the home folder (random scripts, memory files, etc.) instead of `~/workspace/<profile>/`, the cwd is unbound. Fix: `hermes config set terminal.cwd /home/n8n/workspace/<profile>`. This is the #1 cause of "model is looking at wrong files" confusion.
- **`~/.hermes/skills/` base directory is NOT a deployment target for skill copies.** The base `~/.hermes/skills/` exists but only holds cross-profile-only skills (e.g., `kanban-codex-lane`). The `autonomous-ai-agents` and `mcp` categories live in `~/.hermes/profiles/<p>/skills/<category>/`, NOT in the base. When writing a new skill, put a canonical source in your workspace (e.g., `~/workspace/general/<skill>-SKILL.md`) for tracking, then `cp` to each profile's category dir. Don't write to `~/.hermes/skills/<category>/<skill>/` thinking it will be picked up — the skill-discovery loader only scans per-profile skills dirs. Verified with `ls -la ~/.hermes/skills/autonomous-ai-agents/` → empty (no skills in that base dir). See `references/skill-distribution-patterns.md` for the full distribution model.
- **SOUL.md text duplicated via CLAUDE.md/AGENTS.md**: SOUL.md is loaded as identity (slot #1 in stable tier). CLAUDE.md/AGENTS.md are loaded as project context (separate code path in context tier). `skip_soul` only prevents SOUL.md from being loaded twice — it does NOT skip CLAUDE.md or AGENTS.md. If `~/workspace/CLAUDE.md` contains the same persona text as SOUL.md, the model sees it twice: once as "who I am" and once as "project instructions I should follow." Fix: keep CLAUDE.md/AGENTS.md for project-specific instructions (build commands, conventions, architecture), not persona text. Delete workspace-level CLAUDE.md if it only contains SOUL.md content.
- **agent.system_prompt config key**: `hermes config set agent.system_prompt "text"` appends text to the system prompt after the identity block. It augments, not replaces. Useful for injecting directives that must appear in every session without editing SOUL.md. Unlike SOUL.md, this is per-profile (set in each profile's config.yaml).
- **memory.write_approval config flag**: `hermes config set memory.write_approval true` gates ALL memory tool writes (add/replace/remove) to MEMORY.md and USER.md. When true, foreground writes prompt inline for approval; background-review writes are staged for `/memory pending` / `/memory approve` / `/memory reject`. Default is false (writes happen freely). This only gates the built-in `memory` tool — it does NOT affect Holographic/fact_store writes. Stronger than a SOUL.md directive since it's enforced at the tool level. **Per-profile, not global** — must be set in each profile's config.yaml. Use the `hermes-config-bulk-update` pattern to apply across all profiles in one pass.
- **`/memory pending` and `/memory approve` are chat slash commands, NOT CLI commands**: `hermes memory approve <id>` and `hermes memory pending` do not exist — the `hermes memory` CLI only has `setup`, `status`, `off`, `reset`. When `write_approval` is on, pending entries are reviewed with `/memory pending` (lists staged entries), `/memory approve <id>` (approves one), and `/memory reject <id>` (rejects one) — typed directly in the chat, not via the terminal. The `hermes memory status` command shows which provider is active but does NOT show pending entries.
- **Temperature is set in THREE places, not one**: A profile's temperature lives in three independent locations: (1) `moa.presets.<name>.reference_temperature` — the research sub-agent's reference stage, (2) `moa.presets.<name>.aggregator_temperature` — the research sub-agent's aggregation stage, (3) `custom_providers[].extra_body.temperature` — the main model's temperature passed via the Ollama provider's extra_body. All three must be set to the same value for consistent behavior. The main model temperature is NOT a top-level `model.temperature` key — it's embedded in the provider's `extra_body` block. To set all three: `sed` for the MOA temps (simple value replacement), and `patch` or `execute_code` to insert `extra_body: { temperature: 0.3 }` into the Ollama provider block (which may not exist yet). See `references/temperature-config.md` for the full pattern.
## Skills as Dispatcher + Methodology Contracts (Cross-Profile Distribution)
When designing a NEW skill that runs across profiles (i.e., a skill with more than one SKILL.md file), pick the distribution model upfront and document it in the plan. The two main patterns:
### Pattern A: Single skill, multi-profile copies (default)
- One SKILL.md, copied to every profile that needs it as a real file (not symlink).
- Use when: the skill is self-contained and each profile can run it independently with the same behavior.
- Example: `searxng-smart-search` lives at `~/.hermes/profiles/<p>/skills/mcp/searxng-smart-search/SKILL.md` on every profile that wants MCP search. The SKILL.md is identical in every copy.
### Pattern B: Dispatcher + Methodology contract (cross-profile delegation)
- **Two SKILL.md files** with a strict separation of concerns.
- **Dispatcher** (per-profile, lightweight): owns trigger detection, clarifying questions, dispatch command, session-id capture, delivery report. Installed on every profile that needs to fire the trigger. ~150 lines.
- **Methodology** (one specialized profile only): owns the actual heavy logic. Lives on the profile whose tools/model fit the work best. ~300+ lines, may be expensive to run.
- The dispatcher's command is identical regardless of which profile it's running on: `hermes -p <specialized-profile> -s <methodology-name> chat -q "<question>" ...`. The `-p` flag pins the target.
- Use when: the heavy work needs a specific profile's setup (e.g., a research profile with SearXNG MCP and a long-context model), but the trigger should be fireable from any profile (e.g., from dev, family_law, finance).
- Example: `better-search` (dispatcher, 16 profiles) + `better-search-research` (methodology, research profile only). The dispatcher catches `better search` triggers on any profile and always delegates to `research -s better-search-research ...` (using the `research` profile alias at `~/.local/bin/research` — avoids the `-p` flag that some models hallucinate doesn't exist).
- Reference implementation: `references/skill-distribution-patterns.md` — full design rationale, when to use which pattern, and a worked checklist for new-skill plan docs.
### Why two-skill contracts matter
1. **Single source of truth for the heavy logic.** Edit the methodology once → all 16 dispatchers pick up the change.
2. **Cheap dispatcher, expensive methodology.** The dispatcher is ~150 lines and runs in <1s to fire; only the methodology pays the long-context / multi-search cost.
3. **No skill duplication for the heavy part.** Replicating a 300-line methodology to 16 profiles = 4,800 lines of drift-prone duplication. The contract makes this a non-issue.
4. **Trigger coverage is per-profile, logic is per-specialized-profile.** Triggers can vary per profile (e.g., dev profile might add a dev-specific trigger) without touching the methodology.
### Decision: when to use which
- **Self-contained skill, same behavior everywhere** → Pattern A (single skill, real copies).
- **Skill that needs a specific profile's tools/model** → Pattern B (dispatcher + methodology).
- **Default to Pattern A.** Add the contract only when there's a concrete reason (the heavy logic needs a specific profile's setup, or the methodology is too large/expensive to replicate).
### Pitfalls for the dispatcher+methodology pattern
- **Real copies, not symlinks** for the dispatcher. Each profile gets its own dispatcher SKILL.md. The methodology also gets a real file, but on one profile only. (See `references/skill-distribution-patterns.md` for the verification protocol and the **same-category + same-scope precedent check** — the wrong-precedent trap is the most common error here.)
- **The dispatch command is identical across all dispatcher copies.** Hardcode `-p <specialized-profile> -s <methodology>` in every dispatcher's SKILL.md. The operator never types the profile flag.
- **`--max-turns` is a SAFETY NET, not the cap.** The methodology's loop cap is enforced by its own scaffolding (e.g., a `/tmp/<sid>.md` ledger that counts `## Refinement` blocks). `--max-turns` is just the ceiling the agent hits if it goes long. Set it high enough that real questions don't hit it (e.g., 50 for a 3-loop skill, 600 for an exhaustive one). Same logic as `deep-research` pitfall #4.
- **No `--resume` follow-up for cap-bounded methodologies.** If the methodology has a per-dispatch cap (e.g., better-search's 3-loop cap), `--resume` re-enters with fresh `--max-turns` and breaks the cap guarantee. Make every follow-up a fresh dispatch with a new sid, new ledger, and a new budget. Trade-off: the operator loses research context between dispatches — if iterative drilling matters, use a heavier skill (e.g., `deep-research`) that supports `--resume`.
- **Methodology skill must be loadable before dispatch.** The dispatcher should run a precondition check (`hermes -p <specialized-profile> skills list | grep <methodology>`) and abort cleanly with a "skill not installed" message if the methodology is missing. This prevents dispatching a generic unbounded session. (If you drop the precondition check, the runtime will still report `-s <name>: skill not found` clearly — either is fine, just don't silently fail.)
- **Trigger phrases must be explicit.** Avoid generic English verbs (`look into`, `find out`, `search again`) — they collide with normal agent tasks on dev/code profiles. Use phrases that include the skill's own name (`better search`, `do a better search`). Keep the list short (2-3 phrases); more phrases = more collision risk.
- **Cross-profile tests must bracket sparse AND dense profiles.** When verifying a dispatcher works on every profile, pick one profile that has many other cross-profile skills (dense, e.g. `general`) and one that has few (sparse, e.g. `people`). Testing only one end doesn't prove the principle. Run the same test from both ends.
## SOUL.md Symlink Propagation (One File, All Profiles)
Instead of maintaining independent SOUL.md copies per profile, use a single canonical file with symlinks. `load_soul_md()` does `get_hermes_home() / "SOUL.md"` — a simple file read, so symlinks are transparent.
### Setup
```bash
# 1. Write canonical SOUL.md once
# (edit ~/.hermes/SOUL.md with the desired persona)
# 2. Replace all profile SOUL.md files with symlinks
for p in ~/.hermes/profiles/*/; do
rm -f "$p/SOUL.md"
ln -s ../../SOUL.md "$p/SOUL.md"
done
# 3. Verify
for p in ~/.hermes/profiles/*/SOUL.md; do
profile=$(basename "$(dirname "$p")")
if [ -L "$p" ]; then
echo "$profile: symlink OK ($(wc -c < "$p") bytes)"
else
echo "$profile: NOT A SYMLINK"
fi
done
```
One edit to `~/.hermes/SOUL.md` updates all profiles instantly. No copying, no drift.
### Pitfalls
- **The `general` profile may have the default 513-byte SOUL.md** while other profiles have a custom one. Symlinking replaces the default with the custom persona — this is usually desired but verify the canonical file has the intended content first.
- **SOUL.md is for identity/tone/style, not operational directives.** The docs recommend keeping SOUL.md focused on persona. Operational rules (search protocols, validation steps) belong in AGENTS.md or `agent.system_prompt`. A bloated SOUL.md competes with built-in guidance and can cause inconsistent model behavior.
- **`skip_soul` is not a config toggle.** It's hardcoded logic in `agent/system_prompt.py` line 418: `skip_soul=_soul_loaded`. `_soul_loaded` is True when SOUL.md successfully loads as identity, preventing it from being loaded again as a context file. This only skips SOUL.md — it does NOT skip CLAUDE.md, AGENTS.md, or any other context file.
## Audit Checklist Quick Reference
When auditing a partially-completed clone, run these in order:
```bash
# 1. Workspace dir exists?
ls -la /home/n8n/workspace/<name>/
# 2. cwd bound?
grep "cwd:" ~/.hermes/profiles/<name>/config.yaml
# 3. AGENTS.md references correct workspace?
head -5 /home/n8n/workspace/<name>/AGENTS.md
# 4. Cloned cron jobs — check for duplicates that target shared resources
python3 -c "
import json
with open('/home/n8n/.hermes/profiles/<name>/cron/jobs.json') as f:
data = json.load(f)
jobs = data if isinstance(data, list) else data.get('jobs', [])
for j in jobs:
if isinstance(j, dict):
print(f'{j.get(\"id\",\"?\")}: {j.get(\"name\",\"?\")} | enabled={j.get(\"enabled\",True)} | deliver={j.get(\"deliver\",\"origin\")}')
"
# 5. Hindsight bank_id — shared or isolated?
grep bank_id ~/.hermes/profiles/<name>/hindsight/config.json
# 6. Config version current?
hermes -p <name> config check
```
## Config Deep-Clean & Optimization
When the user asks to "update," "fix," or "optimize" a profile's config.yaml, follow this workflow:
### Phase 1 — Read-Only Audit (always first)
Read the full config.yaml and produce a categorized summary BEFORE making any changes:
- **Stale references**: workspace paths pointing to wrong profile, leaked keys (TELEGRAM_HOME_CHANNEL), old config version
- **Unused platform sections**: slack, discord, telegram, mattermost, matrix, bedrock, moa — remove if gateway is stopped and no platforms configured
- **Platform toolsets**: trim to only the platforms actually used (usually just `cli`)
- **Empty values that should be filled**: delegation.api_mode, delegation.reasoning_effort, stt.local.language, timezone
- **Intentionally empty (leave alone)**: dashboard auth, browser cdp_url, cron chronos, terminal docker_*, security blocklist domains, TTS provider-specific fields for unused providers
Present the summary to the user and ask whether to proceed before editing.
### Phase 2 — Clean & Remove
Use `execute_code` with `yaml.safe_load` / `yaml.dump` (sort_keys=False, allow_unicode=True, width=120) for bulk changes in one pass:
- Remove unused platform sections, bedrock, moa, smart_model_routing (if unwanted)
- Remove stale workspace: key, leaked TELEGRAM_HOME_CHANNEL
- Remove stale custom_providers and their model_aliases (e.g., remove Nous entries and all nous-* aliases)
- Trim platform_toolsets to cli only
- Run `hermes -p <name> config migrate` to update _config_version
### Phase 3 — Max & Optimize Settings
When the user says "max" or "full permission" or "don't ask", apply the max/permissive config pattern. See `references/profile-config-deep-clean.md` in the `hermes-config-bulk-update` skill for the full value table. Key changes:
- **Permissions**: approvals.mode off, cron_mode off, hooks_auto_accept true, subagent_auto_approve true, allow_private_urls true
- **Limits**: agent.max_turns 300, max_live_sessions 32, terminal.timeout 600, tool_output.max_bytes 50000, file_read_max_chars 200000
- **Auxiliary models**: set all 18 tasks to explicit provider/model/base_url (not auto) — use local Ollama models (kimi for light, deepseek for reasoning, gemini-3-flash for vision)
- **Fallback**: configure fallback_model + fallback_providers to local Ollama
- **Delegation**: set provider/model/base_url (was empty), max_iterations 300, max_concurrent_children 5, max_spawn_depth 3
### Phase 4 — Test Infrastructure
Before declaring done, test every endpoint referenced in the config:
```bash
curl -s --connect-timeout 3 http://localhost:11434/api/tags # Ollama
curl -s --connect-timeout 3 http://10.0.0.26:8000/v1/models # vLLM epyc
curl -s --connect-timeout 3 http://10.0.0.8:8888 # SearXNG
curl -s --connect-timeout 3 http://10.0.0.22:6333/collections # Qdrant
```
Test auxiliary models respond:
```bash
curl -s http://localhost:11434/v1/chat/completions -H "Content-Type: application/json" \
-d '{"model":"<model>","messages":[{"role":"user","content":"Reply with exactly: hello there"}],"max_tokens":100}'
```
Note: `:cloud` models via Ollama proxy may return empty content with `max_tokens: 5` — use 100+ tokens for a valid response.
### Phase 5 — Empty-Value Scan
Run a recursive scan for all empty/null/0/[] values, then categorize:
- **Must fill**: delegation.api_mode (chat), delegation.reasoning_effort (high), stt.local.language (en), timezone (America/Chicago)
- **Optional**: display.pet.slug (only if pet enabled)
- **Intentionally empty**: dashboard auth, browser cdp_url, cron chronos, terminal docker_*, etc. — leave alone
### Phase 6 — Validate
```bash
hermes -p <name> config check # Config version current, YAML valid
hermes profile show <name> # Model, provider, skills count
hermes -p <name> config # Full config dump
```
### Key Architectural Facts
- **Profiles do NOT inherit from base `~/.hermes/config.yaml`.** Each profile's config.yaml is standalone — merged only with hardcoded DEFAULT_CONFIG at runtime. Nothing falls through from base. A profile must have all settings it needs in its own config.yaml.
- **Credential pool (`~/.hermes/auth.json`) IS shared across all profiles** — even when HERMES_HOME points at a profile path. That's why all profiles can use the same API keys without duplicating them.
- **Hindsight bank_id is shared across cloned profiles** — memories are NOT isolated by default. Change bank_id in hindsight/config.json if isolation is needed.
## Clean Cloned Junk (Workspace + Profile)
Cloning copies EVERYTHING — including the source profile's runtime state, logs, caches, and scratch files. This bloats the new profile by ~800MB+ and carries stale state that can cause confusion. Always clean after cloning.
### Workspace — remove source profile's scratch files
Files cloned from `general/workspace/` that don't belong to the new profile:
- `memories-organizer-log.json`, `memories-org-*.py`, `memories-*-batch.json` — source's cron job artifacts
- `qdrant-collections.json`, `hindsight-validation.md` — source's reference notes
- `stt_server.py`, `claude_code.md`, `jotty.md` — source's reference docs
- `docs/` dir — source's reference docs (how_agent_loop_works.md, etc.)
- `scripts/` dir if empty
- Keep: `AGENTS.md` (after rewriting it for the new profile)
### Profile — remove cloned runtime state and caches
These all regenerate or are stale:
- `gateway.lock`, `gateway.pid`, `gateway_state.json`, `gateway_voice_mode.json` — stale gateway state (gateway is stopped)
- `channel_directory.json` — has source's Telegram/Discord channels
- `interrupt_debug.log` — cloned debug log
- `checkpoints/store/` contents — cloned snapshots (especially if checkpoints disabled)
- `state-snapshots/` — cloned pre-update snapshots (can be 80MB+)
- `logs/agent.log*`, `logs/errors.log*`, `logs/gateway*.log*`, `logs/mcp-stderr.log`, `logs/hindsight-embed.log`, `logs/update.log`, `logs/curator/` — all cloned (can be 120MB+). **Pitfall:** `logs/curator/` is a directory, not a file — use `shutil.rmtree` or `rm -rf`, not `os.remove`. Same for `cache/` and `sessions/`.
- `models_dev_cache.json`, `ollama_cloud_models_cache.json`, `provider_models_cache.json`, `context_length_cache.yaml` — caches regenerate
- `cache/openrouter_model_metadata.json`, `cache/model_catalog.json` — caches regenerate
- `config.yaml.bak.*` — stale backups
- `mem0.json` — if using hindsight, not mem0
- `sessions/*` — cloned session transcripts (start fresh)
- `state.db`, `state.db-shm`, `state.db-wal` — cloned SQLite (start fresh; 90MB+)
- `verification_evidence.db` — cloned
- `.hermes_history` — cloned shell history
- `audio_cache/*`, `pastes/*`, `image_cache/*`, `images/*` — cloned media
### Do NOT remove (functional):
- `bin/` (tirith, uv, uvx binaries — 80MB, functional)
- `lsp/` (language servers — 37MB, functional)
- `skills/` (skills directory — 20MB, functional)
- `cron/jobs.json` (cron job definitions — keep, just pause duplicates per Audit Mode)
- `hindsight/config.json` (hindsight config — keep)
- `.env` (API keys — keep)
- `auth.json` (credential pool — keep, shared across profiles)
- `SOUL.md` (personality — keep)
- `config.yaml` (the config — keep)
**Expected result**: profile dir drops from ~960MB to ~140MB. Remaining size is bin+lsp+skills (all functional).
A full itemized checklist for the entire profile setup workflow (including junk cleanup) is in `references/profile-setup-checklist.md`.
## Cross-Profile Cron Health Audit (Operational)
Beyond clone-time cleanup (Audit Mode step 4 above), you may need to audit cron health across ALL profiles at runtime — "check all cron jobs worked." The `cronjob action='list'` tool only sees the current profile; it does not enumerate other profiles' jobs. You must inspect each profile's `cron/jobs.json`, ticker timestamps, and output dirs directly. This also covers identifying and removing orphaned duplicate jobs (same job_id inherited via clone, sitting dead in a profile whose gateway no longer runs).
Full procedure: `references/cross-profile-cron-audit.md`.
## Remote ACP / VS Code Setup
When Hermes runs in an LXC/container on the network and VS Code is on a laptop, ACP (stdio-based) needs a bridge. See `references/remote-acp-vscode.md` for the full breakdown of options and the fundamental filesystem limitation.
## Post-Config Validation
After any deep-clean or optimization, run a structured validation pass BEFORE declaring done. `hermes config check` only validates YAML parse + version — it does NOT catch semantic errors like wrong endpoint for a model, shared hindsight banks, or missing env vars.
Key validation steps:
1. Test all infrastructure endpoints are live (curl each one)
2. Test primary + fallback models respond (note: `:cloud` models need max_tokens 100+, reasoning models need 500+)
3. **Cross-check model.base_url matches model.default** — the #1 silent failure: model name on one endpoint, base_url pointing at a different endpoint that doesn't serve that model (e.g., `minimax-m3:cloud` with `base_url: http://10.0.0.26:8000/v1` — epyc only serves qwen3.6-35b)
4. Check hindsight bank_id isolation (general=shared, others should be isolated)
5. Scan for empty values that should be filled (delegation.api_mode, stt.local.language, timezone)
6. Check for leftover platform references after section removal
7. Verify .env keys are set for config references (FIRECRAWL_API_KEY, SEARXNG_URL, HINDSIGHT_*)
Surface decision points to the user: hindsight bank isolation, model.context_length, browser vs security allow_private_urls alignment.
Full validation script and cross-check code: `references/post-config-validation.md`.
## Support Files
- `references/skill-distribution-patterns.md` — Cross-profile skill distribution patterns (Pattern A: real copies vs. Pattern B: dispatcher+methodology contract). Worked `better-search` example, decision checklist, real-copy-vs-symlink verification.
- `references/remote-acp-vscode.md` — Remote Hermes + VS Code ACP: SSH tunnel options, filesystem limitation, and workarounds
- `references/profile-setup-checklist.md` — Full 22-step checklist for profile setup (workspace, cron, config cleanup, permissions, max settings, fallback, junk cleanup, validation). Copy this into the new profile's workspace as a tracking doc.
- `references/post-config-validation.md` — Structured post-config validation pass: endpoint testing, model respond tests, base_url/model cross-check, hindsight bank audit, empty-value scan, decision points to surface to user.
- `references/dgx-qwen3-dflash-details.md` — DGX qwen3 DFlash technical reference: endpoint, context_length 131072 (not 262144), 3-stream KV pool, tool calling, performance metrics, testing (needs 500+ max_tokens).
- `references/cross-profile-cron-audit.md` — Cross-profile cron health audit: the cronjob tool only sees the current profile, so filesystem inspection is required. Covers ticker health, run output dirs, orphaned duplicate detection, and cross-profile job removal.
- `references/hindsight-bank-config-apply.md` — Hindsight bank-level config application: API path (`/v1/default/banks/<id>/config`), `updates` wrapper format (flat PATCH returns 422), replication script to copy general's overrides to a new bank.
- `references/soul-md-propagation.md` — SOUL.md system-wide propagation: 23 locations, one-at-a-time workflow, diff-based validation, pitfalls.
- `references/system-prompt-assembly.md` — Full system prompt assembly reference: three tiers, key files, config toggles, SOUL.md vs context files, symlink pattern.
- `scripts/profile-config-automation.py` — Standalone Python script that applies the full deep-clean + max/optimize config pattern. Run as `python3 profile-config-automation.py <profile_name>` after cloning. Handles all config changes in one pass.
- `references/profile-launch-slowdown-diagnosis.md` — Diagnose why a profile takes several seconds longer to launch than others: MCP connection failures, skill count, state.db size, checkpoints. Includes diagnostic script.
- `references/skill-pruning-workflow.md` — Systematic skill pruning: group by likelihood tier, present as numbered list, remove LOW, re-list, remove specific MEDIUM items by number. Covers nested subdirectory cleanup and verification.
- `references/mcp-server-http-sharing.md` — Convert per-profile stdio MCP servers to a single shared HTTP instance with systemd persistence. Eliminates redundant npx processes and cold-start delays across profiles.
- `references/temperature-config.md` — Temperature is set in THREE places (MOA reference, MOA aggregator, Ollama provider extra_body). Full pattern for setting all three consistently.
- `references/telegram-bot-token-conflict.md` — Diagnose and fix Telegram bot token conflicts where messages route to the wrong profile because two gateways compete for the same token.
- `references/deepseek-reasoning-effort.md` — DeepSeek v4 `reasoning_effort` parameter: only two real tiers (`high` and `max`). `low`/`medium` silently map to `high`, `xhigh` maps to `max`. Don't bother with intermediate values — they don't save tokens.
- `references/open-webui-infrastructure.md` — Open WebUI deployment details for this environment: systemd service (not Docker), SQLite config table, ComfyUI wiring, Hermes API server connections, user accounts, and pitfalls.
- `references/open-webui-image-gen-debugging.md` — Open WebUI image generation debugging: the 3-way AND gate (model capability, backend flag, user permission), diagnostic curls, DB-level checks, fixes by gate, frontend cache clearing, and current environment state.