Hermes agents
A Hermes agent is not a program you run. It is a profile directory the Hermes runtime loads:
<hermes_home>/profiles/<name>/
├── SOUL.md identity — loaded every turn
├── config.yaml model, max_turns, platforms, enabled skills
├── .env.example the secrets the profile needs (copy to .env / export)
└── skills/<slug>/SKILL.md
nuvel new <name> --framework hermes stamps exactly that. Installing it means
moving the stamped package directory into <hermes_home>/profiles/<name>/
(default home ~/.hermes, overridable with HERMES_HOME) — the same layout
nuvel bots manages through the hermes CLI.
There is no server, no model loop, and no requirements.txt in a Hermes agent. If you find yourself writing one, you're building an ADK or Buzz agent and should switch frameworks rather than fight this one.
SOUL.md vs config.yaml
The split is the thing most worth internalizing, because getting it wrong is what makes a profile impossible to tune later:
| SOUL.md | config.yaml | |
|---|---|---|
| Holds | who the agent is, what it's for, how it speaks | which model, how many turns, which platforms |
| Changing it | changes behaviour | changes cost, reach, or latency |
| Audience | the model, every single turn | the runtime, at load |
model.default never belongs in SOUL.md, and "be concise in Telegram" never
belongs in config.yaml. SOUL.md is loaded on every turn — keep it short. A
600-line SOUL.md is a per-turn tax on every conversation the agent ever has.
Configuration
model:
default: deepseek/deepseek-v4-flash # bare id — no LiteLLM provider prefix
provider: openrouter
agent:
max_turns: 60
platforms:
telegram:
enabled: false
skills:
enabled: []
Two things that trip people up:
- The model id is bare.
providerselects the endpoint, soopenrouter/deepseek/deepseek-v4-flashis wrong here even though that's the form nuvel's ADK backend uses for LiteLLM routing. Strip the prefix. skills.enabled: []does not mean "no skills." It's a force-load list. Empty is the normal state: every skill underskills/stays discoverable by description, and bodies load on demand. Name a slug there only when the skill must be in context from the first token — each one is a permanent prompt cost.
max_turns bounds model↔tool round trips per turn. If you see it hit in logs,
the agent is stuck in a loop, not working hard.
Anything set here has a hermes config set <key> <value> equivalent
(model.default, …). The file is the checked-in starting point; the CLI is
how you change one knob on a running install without editing YAML.
Skills
Anthropic format: skills/<slug>/SKILL.md, YAML frontmatter with name and
description, then the body. Only frontmatter is loaded up front — that's
what makes fifty skills affordable.
Write the description for a reader deciding whether to open the file: name
the situation ("Read when an alert fires"), not the topic ("about incidents").
It's the only signal available at selection time.
Skills are shared across the nuvel ecosystem — nuvel bots skills list browses
the hub and nuvel bots create --skills <refs> installs them straight into
<hermes_home>/profiles/<bot>/skills/. A skill written for a Hermes profile
drops into Claude Code or an ADK agent unchanged, as long as it doesn't hard-
code a harness-specific tool name.
Telegram
nuvel new <name> --framework hermes --with-telegram swaps in a config.yaml
with the platform on:
platforms:
telegram:
enabled: true
bot_token: ${TELEGRAM_BOT_TOKEN}
dm_policy: allowlist
allowed_users: []
mention_only: true
bot_tokenstays an env reference. The token is the bot — anyone holding it can post as the agent. It belongs in.envor a secret manager, never in a committed config.dm_policy: allowlistis the safe default.openmeans anyone who finds the bot can spend your model budget. Start restricted; widen once you know what traffic looks like.mention_only: truein groups. A bot answering every message in a busy group is expensive and socially exhausting — the same lesson the Buzz backend encodes asBUZZ_REPLY_POLICY=mention.
Slack and Teams are ADK-only. --with-slack / --with-teams are rejected here
rather than silently ignored, as are --persona (a Hermes profile already has
a SOUL.md), --with-composio, --workflow, --with-eval, and --with-acp
(Hermes owns the runtime, so there's no process to hand an ACP adapter to —
use --framework buzz if ACP is what you want).
Debugging
| Symptom | Look at |
|---|---|
Profile doesn't appear in hermes profile list |
it's not under <hermes_home>/profiles/<name>/; check HERMES_HOME |
| Model calls fail with an auth error | the key env for model.provider isn't exported into Hermes' environment |
| "unknown model" from the provider | a provider prefix left on model.default |
| Telegram bot silent in a group | mention_only: true and nobody @-mentioned it |
| Telegram bot silent in DMs | dm_policy: allowlist and the sender's numeric id isn't in allowed_users |
| Agent ignores a skill it should have used | the description names a topic, not a situation — rewrite it for the selection moment |