Skip to content
Go back

The Bot Runs as You: Hardening a ⚕ Hermes Agent for Guests

Edit page

The Bot Runs as You: Hardening a ⚕ Hermes Agent for Guests

2026-11-08

Thesis: a messaging bot is not a separate user. It is your account with a Discord adapter attached, and adding people to it adds people to your shell — so the boundary has to be drawn on the platform, not on the person.

Goal: members keep the bot in Discord and can add nothing that runs; you keep the full toolset over SSH.

Status: config-only. The Discord list below is five toolsets — four read-only, plus skills behind the write gate — and the CLI keeps the full set.

Related: the private Discord setup is the invite side, and the skill index is what fills the library this article gates.

A skill appeared in ~/.hermes

Yesterday a member asked the bot for a custom skill. The bot wrote one, and it landed in /home/jeff/.hermes/skills/. Nothing broke. That was the problem: it worked exactly as designed, and the design is “the agent may modify any skill it can see”.

Skills are files. The agent creates, patches, and deletes them through skill_manage. If that tool is enabled on the platform the guest is talking on, a guest puts a file on your VPS by asking politely.

Why it happened

The gateway runs as jeff. Every tool call — terminal, write_file, skill_manage, cronjob — inherits that uid. There is no privilege boundary inside the process, because there is no second identity to switch to.

So the invite was the mistake, not the prompt. Adding a member to the bot added them to the account that holds your SSH keys, your git config, and your home directory. The private Discord setup is how to issue an invite that does not hand over the account.

The limitation worth knowing first

Hermes configures tools per platform, not per user. Gateway authorization is binary: a user ID is allowed or denied. There is no “this member may run a shell, that one may not” for plain chat — only slash commands split into admin and non-admin.

So everyone on one platform shares one tool list: grant yourself terminal on Discord and every guest has it; deny it and you lose it there too. The shell is the only place you can hold more power.

That decides the architecture. The trust boundary has to be the platform, or a second gateway. It cannot be the person.

Three layers

LayerQuestion it answersMechanism
Identitywho is the process?a dedicated unix user — needs a home outside your own
Capabilitywhat can a guest make it do?a per-platform toolset whose writes are gated
Control planecan a guest undo the above?admin-only slash commands

Three layers: control plane, capability, identity

Identity is the only layer that survives a tool you have not audited yet. Capability is the one that stops today’s incident. The control plane keeps a guest from re-enabling what you disabled.

This article applies the second and third. The first needs a home outside your own: a tree inside /home/jeff cannot coexist with chmod 700 /home/jeff, which would lock the service user out of its own home. Everything below is the config-only path, and the tool list is the barrier.

1. Audit what is already there

# pick the window yourself — the day you noticed, not a date from this article
find ~/.hermes -newermt "-7 days" -type f \
  | grep -vE 'state\.db|/logs/|gateway_state\.json' | head -40
hermes skills list
hermes cron list

Read every new file before deleting it. A planted skill runs on every future use, so a blind rm is not an audit. Then check ~/.hermes/config.yaml for keys you did not write, and .env for values you did not set.

2. Take the writes away

Every toolset that can write, and what it writes:

ToolsetToolsWrites to
filewrite_file, patchany path the gateway uid can reach
terminalterminal, process_managethe same, through any command
code_executionexecute_codethe same, through Python and subprocess
skillsskill_manage~/.hermes/skills/ — create, patch, or delete any skill; staged under ~/.hermes/pending/skills/ when the write gate is on
memorymemory~/.hermes/memories/ — MEMORY.md and USER.md
cronjobcronjob_manage~/.hermes/cron/jobs.json — writes that run later
browserbrowser_exec, browser_cdp, browser_*profile and downloads; browser_exec runs Python
delegationdelegate_taskchildren inherit tools, so any row above
image_gen, video_gen, ttsimage_generate, video_generate, text_to_speechgenerated media files
connectionsmanage_connectionsconfig and MCP servers — which add tools
kanbankanban_*~/.hermes/kanban.db, plus locks and board metadata under ~/.hermes/kanban/
computer_usecomputer_usefiles reached through the desktop
discord_admindiscord_adminroles and channels — Discord, not the filesystem

session_search is not in that table because it writes nothing — but it reads past conversations, so it belongs off on privacy grounds alone.

The selection is the whole list, not a set of removals: whatever you put in platform_toolsets.discord is exactly what that platform gets. Put the gate first and the writer last, in one file:

# ~/.hermes/config.yaml
skills:
  write_approval: true           # nothing installs without your approval

platform_toolsets:
  discord:
    - web        # web_search, web_extract
    - vision     # vision_analyze
    - clarify    # ask a question
    - todo       # in-session task list
    - skills     # skill_view, skills_list — writes staged, step 3

Do not paste that block in as a new section. skills: is already there, and platform_toolsets: is there once hermes tools has written it. A second copy of either makes the file unparseable, and then none of its settings load. Set the keys instead:

hermes config set skills.write_approval true
hermes config set platform_toolsets.discord '["web","vision","clarify","todo","skills"]'

hermes config set writes into the block that already exists, and creates the parent path if the key is not there at all. If it had to recreate platform_toolsets, the cli: and telegram: lists that lived in that block are gone, and both fall back to defaults.

That is “as much as possible” under the rule that nothing installs without you. browser, image_gen, video_gen, and tts look harmless and are not: they write browser profiles, downloads, and media files.

For the maximum instead, discord: [] removes everything, tools and conversation aids alike.

The same selection, if you would rather click than type: tick those five boxes in hermes tools. Set the gate before the restart that applies the toolset — ticking the boxes writes the file, the restart activates it.

hermes gateway restart
hermes tools --summary

A broken config does not fail loudly. One duplicate top-level key makes the file unparseable. Older builds fell back to defaults, and a duplicate skills: that way reverted everything — every tool back, no gate, no lock. Current builds keep serving the last config that parsed instead, reading ~/.hermes/backups/config/config.yaml.good.* in a fresh process. Either way your new block never loads: the hardening you just wrote is silently not in effect, and the permissive config from before it may be the one actually running. After any hand edit, hermes config check must report no error and hermes tools --summary must still show five. Both are one line, and they are the only check that catches a silent no-op.

The CLI keeps its full set either way, so SSH sessions still have everything.

3. The skill gate

Why skills is there at all: members need skills_list and skill_view to see and use the library, and those travel with skill_manage in the same toolset — toolsets are not split by tool. The gate is what keeps authorship yours. Every skill_manage write — create, patch, delete, file write — is staged under ~/.hermes/pending/skills/ instead of installed, and the skill is inert until you act. That directory appears on the first staged write, so ls on a fresh install answering No such file or directory means nobody has proposed anything yet, not that the gate is off.

/skills pending      # staged writes, one line each
/skills diff <id>    # the full diff — read it in the CLI, chat truncates
/skills approve <id>
/skills reject <id>
/skills approval on  # the same switch as write_approval, from chat

Members keep skills_list and skill_view; only your account is admin, so only you approve. The cost is one staged JSON per proposal — a write, but not an installation — plus one review per skill. Approved skills land in ~/.hermes/skills/, so git-init that directory: every approval then has a diff and a revert. Lock what you depend on with hermes curator lock <skill>, and an approved skill appears to a new session; /reload-skills forces a rescan in the current one. What the library costs before anyone proposes anything is the other half of this: the skill index measures it.

4. Close the control plane

allow_admin_from decides who may run slash commands. It has no effect on tools: a slash command is a command, not a capability. Scope matters — allow_admin_from covers DMs, group_allow_admin_from covers server channels, and both are needed.

Both platforms.<name> and gateway.platforms.<name> are read, but the top-level block wins on shared keys, extra included. So put it in one place, next to enabled:

platforms:
  discord:
    enabled: true
    extra:
      allow_admin_from:
        - "<your-user-id>"
      group_allow_admin_from:
        - "<your-user-id>"
      user_allowed_commands:
        - whoami
        - status
      group_user_allowed_commands:
        - whoami
        - status

Write it once. If both blocks carry an extra, the top-level one wins and you will debug the copy that does nothing.

Non-admins get /help, /whoami, and that list. They do not get /yolo, which would switch approvals off in their session.

Slash commands are a second write surface. They are not tools, so platform_toolsets never sees them, and several of them write on their own:

CommandWhat it writes
/savea session export under ~/.hermes/sessions/saved/ (md, json, html)
/plana markdown plan under .hermes/plans/
/initgenerates or updates AGENTS.md
/learndistils a new skill into ~/.hermes/skills/ (staged when the gate is on)
/refine, /curatormemory and skill maintenance writes
/rollbackrestores filesystem checkpoints
/updatereplaces the Hermes install
/sethome, /model --global, /reasoning --globalwrites config.yaml or .env

The admin split is the only thing that gates them. That makes it more important than the tool list, not less.

/tools is not on that list and never was: it is a CLI-only command, so it does not exist in Discord and the tool selection cannot be re-opened from chat at all.

Check the split from the channel, not the shell:

/whoami

Tier: admin means the block was read. Anyone else should get Tier: user and a short list of commands after it; that line is the split working. Tier: unrestricted means no admin list is in effect for that scope — the YAML landed in the shadowed block, or you set the DM pair and are testing in a channel. In that state every command in the table above is open to your guests.

5. Verify

From a guest account, ask for a shell and a file write. Both should fail because the tool is absent, not because the model refused — a refusal means the tool is still there and the model is being polite. Then ask for a skill: it should stage under ~/.hermes/pending/skills/ and install nothing.

hermes tools --summary                          # Discord: five toolsets, nothing installed
find ~/.hermes/skills -newermt '-10 minutes'    # empty
ls ~/.hermes/pending/skills                     # the staged proposal, if any

Run hermes tools --summary in a real terminal — piped, or as a non-interactive SSH command, it refuses with “requires an interactive terminal”.

Confirm your own DM still answers, since the change applies to you on that platform too.

What the finished shape looks like

SurfaceChatToolsWrites
Discord — youyesweb, vision, clarify, todo, skillsstaged only
Discord — membersyesthe same fivestaged only
SSH + hermesyesall of themyes

The fifth toolset is there so guests can see and use the library; the gate is what stops them installing into it. Nothing a guest writes reaches ~/.hermes/skills/ without your /skills approve.

You and the other members are equal on Discord. The only difference is slash commands, and the only place you hold more power is the shell — which is the point.

What this does not buy

Rollback

Re-enable a toolset in hermes tools and restart; /skills approval off ungates skill writes the same way.

The lesson is smaller than the setup: an invite is a grant. The bot does not have a users table. It has a uid.

I will leave this running for a while before calling it settled. The question that matters is not whether the config is correct on paper, but whether the bot stays healthy in a group where other people talk to it: five toolsets, writes staged, nothing installable. Anything practice teaches me that this config does not cover goes here.

btw, i use arch


Edit page
Share this post on:

Next Post
Disable the Skills You Will Never Use in ⚕ Hermes