
What I wanted was for Hermes to stop depending on the right skill happening to be loaded at the right time. I wanted it to stop seeing a familiar problem, missing the reusable thing we had already built, and reinventing the whole approach from scratch.
I wanted Swamp-first Hermes.
Hermes would still be the thing I talk to in Discord. But instead of treating a conversation as an invitation to improvise, it would use Swamp models and workflows as the middle layer: named capabilities, stable inputs, composable pieces, records of what ran, and a real place to look when something goes wrong.
Security was a benefit of that design. Determinism was the reason I cared about it.
The Short Path
The full story was messy. The setup I was trying to reach is not.
First, turn off the broad Hermes toolsets in the configuration used by the running gateway—not merely the first plausible config file you find:
agent:
disabled_toolsets:
- terminal
- file
- code_execution
- computer_use
- web
Hermes 0.19 treats web and browser as separate built-in toolsets. That distinction matters: the configuration above disables generic web access while leaving the direct browser available. I kept it that way deliberately. The preferred path is the agent-browser-batch model and its narrow runBatch method; the published package was independently verified as @mgreten/agent-browser-batch 2026.07.16.1. But the policy is Swamp-first, not Swamp-only. The browser remains a disclosed fallback when a site blocks or times out through the reusable path.
This is not security theater, and the goal is not to make Hermes less useful. Swamp gives repeated research a composable flow and leaves evidence behind. The fallback keeps that preference from turning an ordinary request into a nine-minute demonstration of purity.
Next, install the Swamp-first plugin:
hermes plugins install meagerfindings/swamp-first-hermes --enable
Then enable both the plugin and its toolset. These are separate opt-ins:
plugins:
enabled:
- swamp_first_hermes
platform_toolsets:
discord:
- hermes-discord
- swamp_first
The plugin name has to match plugin.yaml. Enabling the plugin does not automatically expose its tools in Discord, and adding the toolset does not help if the plugin itself is disabled.
The plugin is only the doorway. The useful capabilities still live in Swamp, so search before building and pull the reusable pieces you need:
swamp model type search <query>
swamp extension search <query>
swamp extension pull <package>
Give each repeated job a narrow interface. Use models for the individual capabilities, workflows for the handoffs between them, and local arguments for the deployment-specific details. Hosts, paths, identifiers, schedules, and credentials do not belong in an extension you intend to share.
Finally, test the actual conversation surface. Ask Hermes to perform an ordinary job through Swamp. Then ask it to do something that requires one of the toolsets you removed. The answer that matters is "I don't have a tool for that," not "I'm not allowed to." Everything before the live conversation is configuration. The conversation is the proof.
What the Plugin Actually Does
The swamp-first Hermes plugin is not a second automation system, and it does not contain special GitHub, Todoist, or homelab logic. It is a narrow adapter between Hermes and Swamp. Its twenty-two tools give Hermes a small vocabulary for finding capabilities, authoring definitions, running them, checking the results, and publishing the reusable parts.
The discovery tools let Hermes search installed model types and workflows, inspect a model's methods and arguments, and look through the extension registry before it builds something new. The authoring tools scaffold model and workflow definitions, patch an existing model's configuration, and write only to known Swamp definition or extension paths. Those writes are validated immediately; invalid model and workflow changes are reverted instead of being left half-working.
The execution tools run an existing model method or workflow with structured JSON inputs and pull registry extensions into the local Swamp repository. The release tools format, type-check, test, score, and prepare the content-bound adversarial review for an extension. Public registry publication and activating an unattended workflow schedule are separate operations that require explicit confirmation.
There is no "run whatever Swamp command you want" tool and no shell string hidden underneath it. Each handler has a fixed operation and bounded arguments. When a model method fails, the plugin can return useful diagnostics, but it scrubs local paths before Hermes sees them.
That separation matters. If Hermes commits and pushes Git, it is using a Git workspace model through the generic method runner. If it creates a Todoist project, the trip-packing model owns that behavior and its confirmation contract. The plugin knows how to invoke and inspect reusable capabilities. The models and workflows know how to do the actual work.
The Simple Version Was Not Simple
The command line said the plugin was installed. The tool list said it was enabled. Discord said Hermes had none of the new tools.
Here is what actually happened: I had two homes.
The command line had been looking at one configuration tree. The running gateway was looking at another. I had successfully installed the right thing in the wrong place, then trusted the output that told me it existed. The mechanism was almost funny once I saw it: leave one environment variable, HERMES_HOME, unset, and the CLI quietly falls back to a lookalike home directory with its own config and its own plugins folder — coherent, complete, and not the thing actually running. There was also a missing opt-in setting in the real configuration. Both had to be true before Hermes could see the tools I had built for it.
That particular mistake repeated itself in different forms all day. The visible surface kept making claims that were technically true and operationally useless.
The Errors Were Lying Too
One of the safety mechanisms I wanted was fail-closed behavior. If Hermes wrote an invalid Swamp definition, it should be validated and reverted rather than left half-working on disk.
That part was doing exactly what it was supposed to do.
But the adapter had flattened the useful validation message into a generic process error. The guardrail worked. The explanation disappeared. A single missing error body made three separate problems look like broken infrastructure.
Once we surfaced the real error, they were ordinary fixes.
That stopped being a one-off fix and became a rule. The safety boundary still owns the decision to withhold a failed process's raw output — a stderr blob can carry absolute paths, hostnames, whatever else happens to be on the box. But a blind agent cannot correct a mistake it was never allowed to see the reason for. So the fix became a narrow allowlist: authoring and execution commands whose stderr gets surfaced at all, with every absolute path scrubbed down to something repo-relative, or swapped for an opaque placeholder when it can't be made safe at all. A lint failure, a bad file:line, a rejected argument — all stay legible. Where anything actually lives on disk does not. The same fix later covered model_method_run, workflow_run, and the rollback path when a written definition fails validation. Same shape of bug, two more places it had been hiding.
There were more little seams like that. A publish command returned more structured output than the parser expected, so successful work looked like a failure. The tool that ran model methods could not pass arguments, which meant a whole class of otherwise safe models was unusable. One field meant one thing for models and another for extensions, so following the documentation led directly into the wrong interpretation.
None of these were the point of the project. But they were the work.
This is the part that gets left out of agent demos. You do not get from “I can ask my bot to do something” to “I can expect it to take the same good path next time” by making one elegant configuration change. You get there by finding every place where the system has been silently guessing.
I Broke the Registry
Then I made a contained writer so Hermes could work inside a normal git repository without getting raw file access back.
That was the right idea. It was also how I took down the local model registry.
An old bundling bug meant one bad entry could fail the shared bundle for every local model. My new model hit it. The registry failed, and the automations that depended on it went dark with it.
That was not a fun discovery.
But it clarified something important. The problem was not that the Swamp-first approach was too constrained. The problem was that the runtime underneath it was old enough to contain a bug already fixed upstream. Updating Swamp, rebuilding the image deliberately, and re-adding the model gave us the answer we wanted: the registry stayed healthy and the model methods merged deterministically.
I am thankful for that kind of result. Not because it makes for a clean story, but because it is real. The system failed in a way we could see, we found the actual cause, and the repaired version could prove it was different.
The Upgrade Was Part of the Same Project
At that point, it made sense to update Hermes too.
That was supposed to be a base-image refresh. Instead, Hermes had changed its init system underneath us. The old entrypoint still looked correct, but it no longer started the gateway. A naive upgrade would have produced a container that booted, did some setup, and then exited.
So we staged it: inspect the new base, test a throwaway container against a scratch data directory, keep a rollback image, and only then move production over.
The final fix was simpler than the first plan. Let the new init system supervise the gateway itself, and run the one extra service as a supervised long-running process instead of making it part of boot. That way a failure in the extra service could not keep Hermes from starting.
Again: the surface looked fine until it did not. The actual contract was lower down.
A Permission Fix That Could Be Reused
The FolderSync episode is a small but concrete example of why I wanted Swamp-first Hermes. The same Obsidian task had been approached through a sometimes-loaded skill, then rediscovered when the surrounding assumptions changed. The skill described the right permissions, but the actual model instance had silently fallen back to generic defaults. A successful-looking write was not proof that the synced file was writable.
The durable fix was not another reminder in a prompt. It was a named Swamp model with explicit local arguments: the mounted vault root, 0666 for files, and 0777 for created directories. We then created a real scratch note through that model and checked the resulting filesystem modes. The distinction matters: the model is now the repeatable path Hermes uses for this kind of vault work, and the live output gives us a way to verify that the contract actually held.
It also exposed a boundary worth keeping honest. Hermes runs as its own Unix user, so it cannot repair restrictive permissions on a Mat-owned existing file. The point is not that the model makes every filesystem history disappear. It is that new Hermes-created notes now take the correct path every time, instead of depending on whether a remembered skill happens to be present.
Two Homes, Again
The "two homes" story from the top of this piece was not a one-time bug. It came back, dressed differently, the next time I actually shipped a change to the plugin.
Hermes loads the active user-plugin path at /opt/data/plugins/swamp_first_hermes. In my setup that path was a persistent symlink—and it still pointed at an old v0.5.0 checkout while the managed clone beside it was already on v0.8.0. Pulling the repo I was editing, or updating the managed clone, changed nothing Hermes could see until the live symlink followed the same source. I had the right code, updated and verified, sitting one layer away from where it mattered.
Then I scrubbed a leaked work email out of the plugin's git history and force-pushed the cleaned-up branch. hermes plugins update could no longer fast-forward — the managed clone's history had diverged from the rewritten one — and it needed a hard reset before it would take the update at all.
The correction was small once the paths were explicit: point the active symlink at the managed v0.8.0 package, restart the gateway, and check hermes plugins list again. It reported the user plugin enabled at v0.8.0. Both sides of that link live under persistent /opt/data, so replacing the Hermes image will not quietly send it back to the old checkout.
Same lesson as HERMES_HOME, restated. The last mile of deploying to a live agent is full of paths that look like the real one. The CLI can say installed. plugins list can say enabled. A green test suite can say nothing is wrong. None of that is the deploy. The only thing that has ever actually proven a behavior change reached the agent is a real message to it, answered with the new behavior.
The Last Shell-Shaped Gap
Late in this pass, the last obvious escape hatch turned out to be swamp model edit. It looked like the natural way to let Hermes update an existing model instance's config — globalArguments, vault settings, that kind of thing. It is actually an interactive $EDITOR launcher. No stdin, no non-interactive path, nothing an agent without a terminal can drive.
The tempting fix is a shell wrapper around it. That is exactly the kind of gap this whole project exists to close, not paper over. So instead we built a patch tool, swamp_model_set_config: it reads the instance's raw YAML, deep-merges only the config fields the agent asked to change, refuses to touch identity fields, writes the result back, validates it, and reverts on failure. Hermes can now set config by name — no SSH, no $EDITOR, no human standing in as a keyboard.
That is the whole approach in miniature. Every place the agent still had to jump back to me or to a raw shell was a gap. The fix was never "give it the shell after all." It was a narrow, auditable, swamp-native tool that does the one thing and nothing else.
Finding a Model Is Not Understanding It
The next failure looked like another permission problem. Hermes had already authored a trip-packing extension and needed to put its audited source into an existing empty GitHub repository. Instead it delegated the push to another agent, discovered that agent also had no terminal or filesystem tools, and handed me a shell prompt to finish the job.
But the git capability was already there. @twonines/git-workspace was installed, and it already knew how to clone, inspect, commit, and push. A separate registry model, @goodcraft/github, could even create repositories idempotently. Hermes could search for their names, but the plugin gave it no way to inspect a package before pulling it or describe a model type's methods and arguments. It knew a reusable thing existed without being able to learn how to use it. So it guessed, delegated, and eventually asked for a human.
We added the missing read-only verbs: model type search, model type describe, and extension info. That is not a new capability in the usual sense. It is the ability to understand capabilities that are already present — which turns out to be just as important if composition is the point.
There was one more honest complication. Even the correct git method failed when I invoked it directly because a known Swamp resolver bug tried to load the local standalone contained-git-write model as an extension. model search still looked healthy while type describe, model get, and actual method execution all died on the same bundle error. Again, the visible surface was true and useless at the same time.
So this one was both things: Hermes chose the wrong path because it could not inspect the right one, and the right one was broken underneath it anyway. "No terminal" was not the diagnosis. It was where two missing pieces finally became visible.
Source Code Is Not an Ordinary String
The contained git writer eventually made it through publication, but not in the version we first thought was ready. The adversarial review found two real holes in the thing's central promise: case-variant .Git paths could reach the real .git directory on a case-insensitive filesystem, and nested .git segments were not rejected consistently. We fixed both, added tests, and made the write receipt safe to retain in Swamp's data store. The first hardened publication was 2026.07.22.2; the opaque-write addition below became @mgreten/contained-git-write 2026.07.22.3.
Then a stranger problem appeared. Swamp's source classification was fooled by code-shaped text in comments. A historical example that looked like an extension declaration made a standalone model look as if it extended another type. After that was removed, a comment containing the literal words used to export a model was enough to leave the local catalog's normalized type empty. The code was valid. The package was valid. The comments changed what the loader believed the source was.
That is the kind of failure an agent will not reason its way around. The reusable path has to include boring introspection and truthful diagnostics: what type did the catalog index, what version did it resolve, and what error did the loader retain? Once those answers were visible, both contained-git-write and the ordinary git-workspace model could be described and run again.
Trip-packing exposed the next incomplete seam. Two of its source files contain a literal Swamp vault-expression example, ${{ vault.get(my-vault, TODOIST_API_TOKEN) }}. A model method argument is itself expression-aware, so passing those bytes through the writer tries to interpret the source code before the writer receives it. An attempted escape changed the bytes instead of preserving them. For this publication pass I used an audited, byte-for-byte copy and verified hashes, but that is not the Swamp-first end state.
The missing primitive was opaque content transport: a narrow way to carry arbitrary source bytes through a model input without expression evaluation changing them. The fix is deliberately boring. write_file_base64 accepts canonical base64 plus the expected SHA-256, verifies the digest before touching the filesystem, and then uses exactly the same containment path as the text writer. I proved it by sending all three corrected trip-packing source files—including the literal vault expression—through the published model into both the hub and public clone, then comparing hashes.
The same pass closed two other publication seams. Hermes now has bounded tools that type-check only manifest-declared TypeScript, run only declared sibling tests with package-local read access, and write the exact content-hashed adversarial-review report produced by a fresh dry run. The review writer never publishes; registry publication remains a separate explicit confirmation.
The state at this point is encouraging but intentionally not overstated. The live plugin is v0.8.0 with twenty-two tools. Its method runner also gained structured JSON stdin, because discovering an array-typed contract is not useful if the agent can only send strings. contained-git-write 2026.07.22.3 is published, indexed, and proven through a real opaque write. Trip-packing passes a full Deno check and all four tests, scores 14/14, and has a completed review that honestly records its remaining test-coverage limitation. Its corrected source is public at commit 2ac13a50, and the registry lists @mgreten/trip-packing 2026.07.22.1 with a verified repository.
The live Discord conversation finally proved the part this project was after. Hermes inspected the registry entry, pulled that exact published version, validated the installed model without warnings, and produced a read-only preview in 48 milliseconds for two travelers—including a child—across three days and multiple destinations. The result was correctly uneventful: preview status, no project, no sections, no tasks, and no change to Todoist or anything else. It needed no shell, filesystem, SSH, browser, delegation, fallback tool, or human standing by to finish the job.
Browser automation was the next version of that same proof. From Discord, Hermes used only swamp_model_method_run to call agent-browser-batch's runBatch: it opened example.com, returned Example Domain and the page URL, and closed cleanly. All four steps passed, and the run left a versioned Swamp artifact. There was no alternate browser, shell, terminal, SSH, filesystem access, or delegation hiding underneath that run.
Then an ordinary shopping request corrected one of my assumptions. Hermes reached for its bundled browser and searched directly. I had disabled web, but Hermes 0.19 exposes web and browser separately, and browser was still enabled. Nothing had escaped the configuration. I had misunderstood it.
I tried the strict version anyway. With direct browser access disabled, Hermes proved it could do product research through swamp_model_method_run and agent-browser-batch. It also proved why Swamp-only browsing was the wrong policy. Anti-bot challenges and timeouts crippled the broad search, and nine serial model calls took about nine minutes. Only Best Buy produced useful evidence.
The practical rule now lives in the research skills: try agent-browser-batch.runBatch first, with at most one batch and one targeted retry. If a source is blocked, challenged, or times out, use the direct browser—never delegation—as the fallback. Every claim should say which path actually verified it. Terminal, file, code execution, computer use, and generic web access remain disabled.
The final fresh Discord test was less tidy and more useful. The skills loaded, then Hermes attempted swamp_model_method_run twice. The first call used malformed command objects and was rejected. The second used the correct nested-array shape, but Google presented a CAPTCHA. Hermes fell back to the direct browser and found a live, manufacturer-backed Targus option. It reported the exact tools it used and separated what it had verified live from the more general implications. Model-first routing was proven. The fallback was useful. The whole thing still took about seven minutes.
That run exposed another efficiency problem: Hermes navigated one shared browser session in parallel and searched too many sources. The persisted skills now require nested command arrays, forbid parallel browser_navigate calls in that shared session, cap fallback research at three domains, and stop after one fully verified recommendation plus one useful alternative. Those instructions are configured, but I have not yet proved that final cap in another live Discord run.
I do need to keep one part honest. Hermes did not use the new inspection tools to discover the model's schema. It learned by making failed validation probes, including one unnecessary createProject attempt that was rejected before execution because it lacked the required arguments and confirmation. Nothing was mutated, and the end-to-end capability proof succeeded—but the route Hermes took to understanding that capability still needs work.
That gap showed up more clearly when another agent was asked to edit the existing @mgreten/swamp-club-score extension. It treated the pulled registry package as editable source and asked for terminal and filesystem access, even though the Swamp-first route was already available: inspect the registry metadata, ensure a source workspace with @twonines/git-workspace, make a scoped definition write, then check, test, review the diff, commit, and push. The pieces exist. The maintenance path connecting them is still too easy to miss, which means it needs to become a reusable workflow or playbook rather than another sequence we expect an agent to infer.
The Point Was the Middle Layer
The result is not that Hermes can do less. Its Swamp-backed actions have a narrower, more repeatable path.
It can still take a request in Discord, find a reusable model, run a workflow, author a definition, write it into a repository, commit it through a vetted git model, and publish an extension. But it does not need raw shell and file tools to do those things.
The plugin test suite went from 179 to 281 passing while we worked through the edges. Those tests got us close. The live Discord conversation was the proof: Hermes found the published extension, pulled the exact version, validated it, and ran a real read-only preview without falling back to me or to an open-ended tool.
The part I keep coming back to is that Swamp made this possible without turning it into a giant custom application. It gave me a middle layer between conversation and consequence. Hermes is still the approachable front door. But the things it does can be small, named, repeatable, and inspectable. A model can be reused. A workflow can have a real boundary. A bad write can fail closed. A working result can leave a receipt.
That is not an all-or-nothing migration. It is a direction of travel. Each good model takes one repeated decision out of the agent's memory and turns it into a piece of infrastructure you can inspect, test, and reuse.
I did not finish with a perfect system. The schema-discovery probes and the missed extension-maintenance path make that plain. I finished with a system that is harder to accidentally turn into something I do not understand—and less likely to make up a new path when we already built a good one.
The goal was never to give Hermes the keys to everything. It was to make it useful enough that I can hand it a real problem, composable enough that it can use what we have already built, and constrained enough that I can still see where it went.