Skip to content

Open-source contribution

Hermes Discord Rich Presence

From prototype to upstream: how my own experiment became a privacy fix merged into someone else's plugin, a safer default it now ships, and a lifecycle fix proposed to Hermes itself.

Built for
Hermes Agent's community Discord plugin, maintained by DarRahman, and Hermes Agent itself
When
September 2026

The situation

I wanted Discord to show what my AI agent was doing — thinking, using a tool, which model — without giving away what I was working on. A status line looks harmless, but a session title alone can reveal a project. The real question was what an agent should reveal at all.

What I built

I built my own version first, which taught me where the hard parts were. Then I found that the community already had an established plugin. Instead of publishing a competitor, I used my version as a reference to check theirs: I reproduced every suspected problem, threw out the ones that did not hold up, and sent the maintainer a small, tested fix for the one that mattered most.

Unpaid open-source contribution. The Discord plugin is DarRahman's; my work there is one merged pull request and one proposal the maintainer adopted. I used coding agents throughout, and treated what they produced as a hypothesis until I had reproduced it.

The full story

I set out to show what my AI agent was doing in Discord. It turned into a question about what an agent should reveal, a discovery that someone had already built it, and a lesson in when to stop building my own.

Chapter 01

A status line that says too much

Discord can show a short line under your name about what you are doing. I wanted it to show my Hermes agent at work: which model, roughly how many tokens, whether it was thinking or running a tool, and for how long.

But that line is public to everyone in your servers, and a session title alone can give away what you are working on. So the question was never just how to put text in Discord. It was how to show an AI agent's activity without revealing more than the person using it meant to.

Chapter 02

Building my own, to learn the problem

I started with my own implementation. It never became a product. Its value was that it made me solve the hard parts myself, which later let me judge someone else's version on evidence rather than taste.

Technical detail · the reference design

It speaks Discord's local IPC protocol directly with Python's standard library — handshake, SET_ACTIVITY, clear, ping and close — with a total deadline on every exchange and a bounded frame length. Hook callbacks only touch locked in-memory sets keyed by session and turn; a background worker publishes. State precedence is fixed: Delegating › Using tools › Working › Idle › cleared. An OS file lock elects a single Discord writer across profiles, and a lease expires stale state if a process dies.

Building it also exposed a lifecycle gap in Hermes itself — see chapter 9.

Chapter 03

Someone had already built it

Then I found that Hermes already had an established community plugin for exactly this, maintained by DarRahman (opens in a new tab).

The easy move was to publish mine anyway because I had already built it. The useful one was to ask: can anything I learned make theirs better?

A second, near-identical plugin splits users and effort. So I stopped treating my version as something to ship and started using it as a measuring stick.

Chapter 04

Audit, don't assume

Comparing the two surfaced a list of suspicions. A difference from my design is not automatically a bug, so I tried to reproduce each one — and treated a suspicion that failed to reproduce as a result worth keeping, not an embarrassment to hide.

Only the first became an upstream fix. The blocking and re-publishing behaviour reproduced, but they are architectural choices the maintainer is entitled to make, so I did not file them as bugs.

Chapter 05

The privacy bug, and the fix that was merged

The plugin had two privacy settings, and both looked like they worked: turn one on, and the activity disappeared from the main lines. But Discord shows more than those lines. The plugin still wrote the activity — say, “Running Terminal Command” — into the tooltip you see when you hover the app icon. Pick a setting below to see it.

Technical detail · what changed in the code

The tooltip (Discord's large_text) was built unconditionally from the current status:

large_text = f"{self.large_text_template} — {self.current_status}"

The fix applies the same rule the details line already followed. A neutral lifecycle word such as “Thinking” is not a tool name, so it stays, keeping hide_tool_status scoped exactly as documented:

status_names_tool = self.current_status not in LIFECYCLE_STATUSES
show_status = not stealth and not (hide_tool and status_names_tool)

No configuration, dependency or payload-shape changes; the default configuration is byte-identical.

Chapter 06

Proving it, not hoping

A two-line fix is easy to write and easy to get subtly wrong. Before sending it, I made the bug fail a test, made the test pass, and then checked that nothing else moved.

v1.2.2 also carries the maintainer's own dependency fix; the privacy fix is the part that is mine. Being listed as a contributor is credit for that work, not a role in running the project.

View merged pull request #6 (opens in a new tab)
Technical detail · the full check list

Two of the new cases pass on the original code too, on purpose: they pin the unchanged default output and the documented scope of hide_tool_status, so an over-broad fix would fail. Also run: python -m py_compile, hermes plugins validate (ok, no warnings) and hermes plugins doctor (manifest, import and all five hooks registered). The real-client check used a throwaway Hermes home so no existing configuration was touched.

Chapter 07

Changing the default, not just fixing a leak

Fixing the tooltip closed a leak, but it left a bigger question. The plugin could already hide the session title — it just didn't, unless you went looking for the setting. Out of the box, it published the one thing most likely to say what you were working on.

So I opened an issue rather than a patch: a question about defaults is the maintainer's to decide. I proposed that new installs show a generic “Active Session” instead, and that model, tokens and tool state stay visible, because they are what make the status useful. The maintainer agreed, went further than I asked — an unset value now falls back to generic too — and shipped it.

The proposal is mine; the code change is the maintainer's. Read the issue and the maintainer's reply (opens in a new tab).

Technical detail · the three title modes

privacy.session_title_mode already accepted three values. Only the shipped default changed:

full     Session: <title>   ·  [Running Tool] <title>
generic  Active Session     ·  [Running Tool]
hidden   (omitted)          ·  (omitted)

v1.2.3 sets generic in both config.yaml and the code fallback, so a config that omits the key or predates it fails closed. A new test asserts the fallback; the suite stands at 34 passing.

Chapter 08

A second bug — and why I didn't open a PR

Installing the plugin on my Mac, Hermes refused it with a dependency conflict. The surprise was the cause: even on macOS, the installer checks every platform the dependencies might ever run on, including Android. On that path, Hermes and the plugin disagreed about one library.

I reproduced it and found a workaround. But before opening a pull request, I checked upstream — and the maintainer had already fixed it, more cleanly, by removing the plugin's redundant requirement. So the right contribution was no contribution.

Technical detail · the resolver detail

uv resolves one lock file for every environment, so it also evaluated Android with Python 3.14. There, Hermes core pins psutil to a different source than the plugin's own psutil>=5.9.0,<8, and plugin admission failed. PR #7 — authored and merged by DarRahman — removed the plugin's psutil requirement and relies on the one Hermes already provides.

Chapter 09

A gap in Hermes itself

Separately from the plugin, building my own version exposed a rule Hermes did not always keep. Plugins are told when an AI turn starts, and expect to be told when it ends. On a few unusual exit paths — an early return, an error, an interruption — the end was never announced.

Discord presence mostly recovers on its own, because it refreshes periodically. The observers that really need a balanced start and end are others — cleanup and metrics — which is why I proposed this as a general fix to Hermes rather than a Discord workaround.

The rule: if Hermes announces a start, every way out must eventually announce the end — once, never twice. This pull request is open as of 27 September 2026.

Technical detail · how the fix works

A small ContextVar-scoped helper records each observed pre_llm_call and, if run_conversation returns or raises with that start still pending, emits a fallback on_session_end carrying the same identifiers. Normal finalization marks itself first, so there is never a duplicate. Concurrent turns on different threads or asyncio tasks each keep their own record.

51 tests pass across 9 files. The tests were also checked by breaking the fix on purpose: disabling the start record fails 6 cases, and removing the finalizer's mark fails exactly the one duplicate case.

Chapter 10

What I actually contributed

Five items on GitHub are part of this story. Three are mine: a merged fix, an adopted proposal and an open pull request. Two are the maintainer's, and are labelled that way.

Statuses checked on GitHub, 27 September 2026

Chapter 11

How I worked

I use coding agents heavily — Claude Code, OpenCode, Codex. I treat what they produce as a hypothesis until the behaviour is reproduced. That mattered here: some early concerns held up and others did not, and the contribution came from that verification loop, not from accepting generated code at face value.

My part was the judgment around the code: defining what the behaviour should be, directing the investigation, challenging claims that had not been shown, deciding what was worth sending upstream — and whether it should be a patch or a question — and reviewing every diff and test before it left my machine.

Chapter 12

What I'd keep

  1. 01A different design is not a bug.Blocking calls and re-publishing reproduced, but they were choices, so I didn't file them.
  2. 02Try to disprove your own hypothesis.The file-handle leak looked plausible in the code and did not survive a test.
  3. 03Check privacy at the last step.Every setting looked right internally; the leak was in what finally reached Discord.
  4. 04Defaults are the real privacy policy.Most people never open the settings, so the default title mode mattered more than the option to change it.
  5. 05Search upstream before you patch.The install conflict was already fixed; a second PR would have cost the maintainer time.
  6. 06A small change upstream beats a new project.One fix and one proposal reached every user of the existing plugin.
  7. 07Integrations depend on balanced events.A start without an end is invisible until an observer relies on it.

Under the hood

The tools this is built with. You do not need to know any of them to use it — open one if you are curious what it does.

If you're considering working together

How a project actually goes

No jargon, no long contract to decode, and no stage where you are asked to approve something you cannot picture.

  1. 01

    We talk about the problem

    Not the software — the problem. What is slow, what gets forgotten, what people keep having to redo by hand. Usually one call is enough to see the shape of it.

  2. 02

    I write down what I heard

    In plain language, before any building starts, so you can correct me while it is still cheap to correct. You approve it or you don't.

  3. 03

    You see it early and often

    Working pieces, not screenshots or promises. If something is wrong, you say so while there is still time to change it.

  4. 04

    It goes live and stays supported

    Hosting, domain and setup handled. The Valley O'Ville site has been running since 2023 — I don't disappear after launch.

Have something in mind?

Tell me what is not working. If I'm not the right person for it, I'll say so and point you somewhere better.