Skip to content

Profiles

A profile in Hermes-Relay is an upstream-Hermes agent directory — an isolated Hermes instance on your server with its own config, model, and identity. When you pair with a server, the phone auto-discovers every profile on that server and exposes them in the agent sheet — a bottom sheet that opens when you tap the agent name in the Chat top bar.

The three layers

LayerWhat it picksScopeWhere it lives
ConnectionWhich Hermes serverOne pairing per serverTop-bar chip on the left (hidden with a single connection)
ProfileWhich agent on that serverPer chat turn, clears on Connection switchAgent sheet — tap the agent name in the top bar
PersonalityWhich system-prompt presetPer chat turnAgent sheet — same sheet, below Profile

Pick in order: the server (top bar), then the agent + persona (agent sheet). Switching either Profile or Personality shows a toast confirming the new selection.

Where profiles come from

Upstream Hermes profiles live at ~/.hermes/profiles/<name>/ — each is a full, isolated Hermes environment with:

  • Its own config.yaml (model, personalities, provider keys, everything)
  • Its own .env (API credentials)
  • Its own SOUL.md (the profile's identity / system prompt)
  • Its own sessions, memory, skills, cron jobs, state database

See the upstream docs: hermes profile create, hermes profile use, hermes -p <name> <command> — each profile gets its own CLI alias.

Create them with:

bash
hermes profile create mizu
mizu setup

Or clone from an existing profile:

bash
hermes profile create coder --clone

The phone doesn't create profiles — you do that on the server. The phone just picks them up on the next pairing (or the next auth.ok round-trip after a relay restart).

What "switching profile" does on the phone

When a profile is selected, the phone first checks whether the relay advertised that profile's own Hermes API server.

  • With a profile API server: chat, session browsing, memory, tools, model, and SOUL come from that profile's routed API. The chat session drawer clears and refetches through that profile route, so you see that profile's sessions instead of the default agent's sessions.
  • Without a profile API server: the app falls back to the compatibility overlay. It sends the profile model.default and SOUL.md on each chat turn, but memory, sessions, tools, and provider auth still come from the active Connection.
  • Voice: relay-owned voice routes receive the selected profile too. Voice Settings shows whether TTS/STT, streaming voice output, or realtime voice came from profile config or fell back to relay/global defaults. Saving voice output or experimental realtime settings while a named profile is active writes that profile's voice_output: / realtime_voice: section, so profiles like mizuki and victor can keep different voices.

If you want true profile isolation, run that profile's gateway as its own service on its own port:

bash
hermes -p mizu platform start api --port 8643

Then make sure the relay advertises that API server in the profile metadata, or add that gateway as a separate Connection on the phone. Each routed profile API has its own sessions, memory, and state because it is a distinct gateway.

Picker behaviour

  • Hidden when empty. If the server has no ~/.hermes/profiles/*/ entries (and just the default root config), the Profile section of the agent sheet doesn't render.
  • "Default" option at the top of the Profile list. Selecting it clears the override and uses the server's config.yaml/model.default.
  • Disabled mid-stream. You can't switch profile during an in-flight chat turn.
  • Persisted per Connection (v0.7.0). Your pick survives app restart and follows the Connection it was made on — switching to Connection B brings up B's last-selected profile (or its default if never set), switching back to A restores A's selection. Removing a Connection also clears its remembered pick.
  • Jump from Settings. The "Active agent" card at the top of Settings summarizes the current Connection / Profile / Personality and navigates straight to Chat with the agent sheet pre-opened.

Runtime metadata (v0.7.0)

Each profile row in the agent sheet now shows what the relay observes about the profile on disk and at runtime:

  • Status dot (green vs grey). A 6 dp dot rendered next to the profile name. Green when the relay has recently probed the profile's gateway and got a response; grey when the probe is idle, stale, or the gateway isn't running. Gateway-off profiles stay selectable — the probe is best-effort and can be wrong across a server restart, so we hint (50% alpha row) rather than disable.
  • "N skills" chip. Shown when skill_count > 0. Counts the skills visible inside the profile directory's skills root. Hidden when zero. Useful for picking "the profile that has the scheduling skill" at a glance.
  • "SOUL" badge. Shown when the profile has a non-empty SOUL.md on disk. Decoupled from whether the system-message content actually loaded — a SOUL badge means "the file exists and isn't empty", an active SOUL in chat means the server actually served the content.

When a profile with a non-empty SOUL is active AND you pick a non-default personality, the agent sheet adds an inline "Profile SOUL overrides personality while active" caption under the personality section, mirroring the existing note under the profile section. Both are kept so the precedence rule is visible from either side of the sheet.

All three indicators are optional on the wire — if you're paired with a pre-v0.7.0 relay that doesn't report them, the dot renders grey, the chips stay hidden, and the badge doesn't appear. Nothing else changes.

Profile Inspector

From the Settings tab, tap the Inspect Agent card (directly under Active Agent) to open a full-screen viewer for the currently-selected profile. Four tabs:

  • Config — the profile's config.yaml rendered as a collapsible JSON tree. Nested objects collapse by default; tap to expand. Values render in monospace. The file path is shown at the top as a caption so you can cd to it from a shell if you want to edit.
    • Secrets are masked by default. Any value whose key name contains key, token, secret, password, or credential (case-insensitive) renders as abcd...wxyz for values ≥12 chars or ******** for shorter ones. Tap the eye icon next to the value to reveal it for that row. Reveal state is session-scoped — leaving the screen wipes it. Numbers and booleans are never masked.
  • SOUL — the profile's SOUL.md rendered as markdown. The </> toggle in the top-right of the pane flips between rendered and raw monospace source. Byte size + file path show above the content.
  • Memory — one card per file under the profile's memories/ directory (non-recursive). Each card shows the filename and byte size; tap to expand and see its content.
  • Skills — every skill visible to the profile, grouped by category. Each row has a Switch for enabling/disabling the skill.

Editing (v0.7.1+)

Both SOUL and Memory panes now support in-app edits. Tap the pencil icon in the pane (or card) header to enter edit mode — content renders in a monospace editor with a line-numbered gutter. Bottom bar has Save and Cancel; Save PUTs to the relay, reloads the pane with the fresh content, and surfaces a brief "Saved" snackbar. Save failures keep you in edit mode so you can retry.

For memory entries, the + New entry button at the bottom of the Memory tab opens a filename prompt (must end in .md, no slashes, no leading .) and drops you into an empty editor. A filename that collides with an existing entry is rejected; edit the existing entry via its per-card pencil instead.

Skill toggles

The Switch next to each skill PUTs to /api/skills/toggle when tapped. Relays that haven't shipped the real implementation yet return 501; the app shows a "Skill toggle not yet supported on this server" snackbar, reverts the Switch visually, and ghosts out every row's toggle for the rest of the session with an "Enable/disable requires a newer server" caption under the list.

Very large files (SOUL or a memory entry) are still truncated server-side; when that happens the tab shows a banner noting only the first slice is visible — the editor refuses to open on truncated content so you don't accidentally overwrite the tail. Use the Refresh icon in the top bar to re-fetch every tab, or Retry inside a tab's error state to refetch just that one.

Picker naming

The Profile picker in the Agent sheet (Chat top-bar → tap the agent name) uses these conventions:

  • Server default — the no-override row at the top. Clears any active profile pick and lets the server use its config.yaml/model.default. Renamed from "Default" in v0.7.1 so a profile literally named default doesn't collide with this row.
  • Actual profiles show their description as the primary label when present (readable names like "Victor" rather than directory names), with the profile key as a tertiary caption. A "• Running" or "• Idle" text label accompanies the existing green/grey status dot, and screen readers announce "Gateway running" / "Gateway idle" on the dot itself.
  • When the server emits a profiles.updated push (profile added, renamed, or removed on the server side), the app applies the new list immediately and shows a brief "Profiles updated" snackbar. A profile you had selected that the server then removes falls back to Server default automatically.

The Settings card is visible whether or not a profile is currently active; when there's no active profile, the card renders at half opacity with "No active agent" and does nothing when tapped.

Disabling discovery on the server

If you want Connections-only semantics (for example, a minimal deployment where every profile is its own Connection), set this in the relay's config:

yaml
relay:
  profile_discovery_enabled: false

Restart the relay; the picker stays hidden and the app treats the install as single-agent.

Profile + Personality interaction

If you select a profile AND a personality, the profile wins — its SOUL.md is sent as system_message, not the personality's prompt. That's a deliberate choice: a profile is a richer concept (whole identity), and picking both implies you want the profile's full persona. Pick one at a time to keep behaviour obvious.

At a glance

  • Connection = a whole Hermes server.
  • Profile = a named agent on that server, discovered from ~/.hermes/profiles/. Picking one overlays its model + SOUL for chat turns.
  • Personality = a system-prompt preset within the agent's config.

See Connections for the server-level concept and Personalities for the preset-prompt layer.