When Hermes Says It Attached the File and Your Platform Gets Nothing

The worst kind of bug is the one where the agent is confident and the user sees nothing. You ask Hermes to send a report to your own chat platform — a mobile app, an internal tool, a bridge you wrote yourself — and it answers “sent, the file is attached”. Nothing arrives. You ask again, the agent apologises and re-sends, and still nothing arrives, because the assistant has no way to know the attachment was dropped somewhere under it.
The symptom and the exact error
The platform in question is not broken. It implements send_document, send_image_file and send_video, and its sender handles media_files. It is simply outside the set of platforms the send_message tool treats as media-capable. Ask it to deliver media only and the tool refuses outright:
send_message MEDIA delivery is currently only supported for telegram, discord, matrix, weixin, signal, yuanbao, feishu, whatsapp, slack and qqbot; target mobile had only media attachments
Send text and a file and you get the quieter failure: the text goes through, the attachments are dropped with a warning that only shows up in the tool result. From the user’s side the message looks sent, which is exactly why the reporting maintainer described the outcome as their agent repeatedly telling a user “the file is attached” while nothing arrived.
Where the decision is made
tools/send_message_tool.py answers the question “can this platform receive files?” in two places, and only one of them is about your plugin:
- A human-readable note, used in that error message:
telegram, discord, matrix, weixin, signal, yuanbao, feishu, whatsapp, slack and qqbot. - A dict that actually enables the plugin path, which is much shorter:
_PLUGIN_STANDALONE_MEDIA = {
"discord": ("Discord", False, True, [], False),
"feishu": ("Feishu", True, False, None, False),
"slack": ("Slack", True, True, [], False),
"whatsapp": ("WhatsApp", True, True, None, True),
}
Each value is a tuple of (label, discover_plugins_first, caption_capable, media_files_sentinel, pass_force_document), per the comment above it. The list exists because building files into those four platforms’ payloads needs knowledge the generic sender does not have — forced discovery of the plugin registry, caption handling, or a particular shape for non-final media chunks.
Any plugin platform not in that dict falls through to the generic text-only path: text survives, attachments are dropped, and a media-only call fails as shown above. Nothing in the plugin’s own code is consulted, which is the part that stings — the platform can be fully capable and still be invisible to the decision.
Why “just add the platform” is not the whole fix
The reporting maintainer did the obvious experiment: add a tuple for their platform locally (("Mobile App", True, True, [], False)) and the tool routes the attachment through the plugin’s standalone_sender_fn. The file arrived as a media frame — kind=document, mime=text/markdown, byte-identical to the original. So the plumbing works; it is the gate that does not.
But a hard-coded dict means every future plugin platform needs a core edit before it can send a file, and the person who knows the platform is media-capable is never the person editing the core. The issue proposes the obvious exit: let the plugin declare it in its registry entry — something like media_files_supported and a captionable flag, read where _plugin_standalone_sender resolves the sender — so capability travels with the plugin instead of living in a core allow-list.
There was a second layer to that bug hunt, and it is a useful reminder for anyone writing a plugin adapter: the maintainer also found two adapter-side errors, fixed in version 1.1.1 of their adapter. Media descriptors arrive as (path, is_voice) tuples, and their sender had treated them as plain strings, so no file was picked up. In other words, “my platform is media-capable” needs to be true end to end — the tool has to route the attachment and the adapter has to understand what it receives.
If you hit this today
- Check the tool result, not the chat. When an agent reports a file as sent, the reason an attachment vanished is written into the
send_messagetool result, not into the assistant’s reply. If you are debugging a bridge of your own, that string is the first thing to read. - Keep text and media separate while you work. Text-only messages always work on a plugin platform; a media-only send fails loudly, which is actually more useful for diagnosis.
- For plugin authors: track the capability question in your registry rather than assuming it, and make sure your
standalone_sender_fnhandles the tuple shape it is given. If you need attachments today, your options are a local patch to the dict in your own Hermes checkout, or waiting for a core change — neither is something users should have to do quietly. - For everyone else: if you use a plugin platform and file delivery matters, file or vote for the registry-declared flag. A per-platform entry in a core dict is a maintenance burden that a declared capability removes.
Status
Reported as issue #139354 on October 15, 2026, with a reproducible case against a mobile plugin and the locally verified fix described above. As of this writing the core behaviour is unchanged: the four-platform dict is what decides, and the issue’s suggested design — a capability declared by the plugin — is a proposal, not a shipped change.
Worth keeping in perspective: send_message is one tool, and this is one of its gates. If you are building out a plugin platform more broadly, the hermes-mobile ecosystem entry is a working example of an adapter that had to fix this class of problem from the platform side, and the hermes send command reference covers the CLI surface that the tool wraps.
Sources: issue #139354 in the Hermes Agent repository (including the reporter’s local verification and the adapter fixes in version 1.1.1); tools/send_message_tool.py on main for _PLUGIN_STANDALONE_MEDIA and the media-platform note used in the error message.