config.yaml takes precedence, .env is the fallback. The recommended
pattern keeps secrets in .env and references them from config.yaml with ${VAR} (the loader
expands them), so config lives in one declarative file.
Most config.yaml keys have a matching MSTEAMS_BRIDGE_* env var (e.g.
realtime.azure_endpoint ↔ MSTEAMS_BRIDGE_AZURE_ENDPOINT, secret ↔
MSTEAMS_BRIDGE_SECRET); config.yaml wins where both are set. Exceptions: path is
config-only, and the SharePoint site id uses TEAMS_SHAREPOINT_SITE_ID (shared with the
chat plane).
config.yaml
%LOCALAPPDATA%\hermes\config.yaml (Windows) / ~/.hermes/config.yaml:
.env
%LOCALAPPDATA%\hermes\.env / ~/.hermes/.env - the secret store (used directly, or referenced above).
A fully env-only setup works too:
Key reference
Hermes implements the same feature set as the OpenClaw plugin; keys aresnake_case and each has a
matching MSTEAMS_BRIDGE_* env var.
Inbound policy (voice): Hermes vs OpenClaw
Hermes has noinboundPolicy key and no pairing mode for calls - the allowlist is the
inbound policy:
allowlistset → only those AAD object ids may call (addallowlist_allow_names: trueto also match display names, which is weaker).allowlistempty → all inbound callers are denied, unless you explicitly opt out withallow_all: true(env:MSTEAMS_BRIDGE_ALLOW_ALL). An unset allowlist never means “open to all”.- When
allowlistis empty, it inheritsTEAMS_ALLOWED_USERS(the chat plane’s allowlist), so one AAD list can cover both voice and chat.
openclaw pairing approve msteams <CODE> steps you will see in the Quickstart and Teams app
pages are OpenClaw-only. The Hermes equivalents are: edit allowlist for voice callers, and
for Teams chat DMs (handled by the Hermes gateway’s Teams platform, not msteams_bridge) either
approve the pairing code the bot sends an unknown sender (hermes pairing approve teams <CODE>) or
set TEAMS_ALLOWED_USERS - see
the bot ignores Teams chat.
Run
Show config + readiness:--handler streaming for streaming mode, which needs ffmpeg):
StandIn dials the public addresses, so both have to be reachable from the internet. There is no
port in either public URL when you mount by path, which is the documented way to expose them - see
Expose your agent for the two commands and the probe recipes.
Microsoft Graph permissions
The bot’s Azure AD app needs admin-consented application permissions - see the canonical Graph permissions table on the Create the Azure bot page.Gateway-managed mode (two layers)
To havehermes gateway run host the voice bridge itself (one process, no
separate serve), enable both the plugin and the platform:
deliver=msteams_bridge places a call and speaks the result. Set
MSTEAMS_BRIDGE_HOME_AAD to the AAD object id the gateway should call for those
deliveries (the callee must also be on the allowlist). Run gateway mode
or standalone serve, never both - the second owner fails loudly on the
port bind.
Voice & language tuning lives in Hermes
STT and TTS provider selection, voices, and language hints are host configuration, not plugin keys: set Hermes’s ownstt: and tts: blocks in
~/.hermes/config.yaml (see the Hermes TTS and voice-mode feature docs). The
plugin inherits them - streaming mode dispatches the host’s text_to_speech
tool and transcription chain, so all Hermes STT/TTS providers apply unchanged.
The call also answers with the host’s SOUL.md identity and knows the host’s
installed skills (chat/call parity).