Hermes Line
A phone line for your Hermes agent. You ask Hermes to call a business, approve the call, and Hermes dials it
from your own Twilio account. It works the phone menu, waits on hold without talking, and when a person picks up
it either rings your phone and bridges you in, or finishes the task itself. It posts a summary to the chat that
asked for the call. A business that calls back can be answered on the same line. SMS verification codes from
your Android phone can be sent into a call, but only after you approve exactly which call gets which code.
Version 0.1.0. Requires Hermes 0.22.5 or newer. License: MIT (LICENSE), copyright Can Uzay.
Quick start (about 5 minutes)
You need Hermes 0.22.5 or newer and a Twilio account. The plugin places no call before step 7; only Twilio's own
caller-ID verification in step 1, if you choose it, rings the phone you verify. Each step is explained in full
under Setup.
- Twilio. In the Twilio console, buy a US local number with voice (the line number) and create an API key
(its SID starts withSK; keep the secret). A Twilio trial account can call only numbers you have verified in
Twilio. To show your own mobile number to businesses, verify it under Verified Caller IDs (optional; Twilio
calls that phone and you type the code it shows). - Install.
hermes plugins install uzay-eth/hermes-line/plugin --enable. Hermes asks for
HERMES_LINE_TWILIO_ACCOUNT_SID(AC...),HERMES_LINE_TWILIO_API_KEY_SID(SK...) and
HERMES_LINE_TWILIO_API_KEY_SECRET. - Tunnel. With
cloudflaredinstalled, runcloudflared tunnel --url http://127.0.0.1:8788and leave it
running. It prints anhttps://...trycloudflare.comaddress: that is yourpublic_base_url(a new one on every
run). - Call profile.
hermes profile create hermes-line-calls, thenhermes --profile hermes-line-calls model
(no--clone). Add to~/.hermes/profiles/hermes-line-calls/config.yaml:platform_toolsets: acp: [] memory: memory_enabled: false user_profile_enabled: false plugins: enabled: [] - Settings. In your Hermes
config.yaml(numbers in E.164,+1and ten digits), then restart the gateway
(hermes gateway restart, orhermes gateway runin a terminal):plugins: entries: hermes-line: settings: enabled: true public_base_url: "https://<your-tunnel>.trycloudflare.com" from_number: "<your Twilio number, or your verified mobile>" line_number: "<your Twilio number>" owner_number: "<your mobile>" owner_name: "<your first name>" call_profile: hermes-line-calls policy: owner_timezone: "America/Denver" # yours allowed_destinations: - "<your mobile>" # add each business later - Check the setup.
hermes hermes-line checkin a shell, or/line checkin a Hermes chat. It places no call
and costs nothing: it asks Twilio only read-only questions (are the credentials accepted, is the line number on
the account with voice, is the caller ID yours) and prints one line per item,OKor what to fix. The shell
command exits 0 only when every item passes. - First call, to your own phone. Outside quiet hours (21:00 to 08:00 in
owner_timezoneby default), ask
Hermes in a chat: "Use Hermes Line to call and tell me the line works." Hermes posts an approval
request; reply/line approve <task_id>. Your phone rings and Hermes speaks the AI disclosure first. Then add a
business number toallowed_destinations, restart the gateway and ask for a real call.
Status: what has been shown, and how
This is a hackathon release. Every claim below names its evidence in this repository.
| Feature | Status | Evidence |
|---|---|---|
| Outbound call, AI disclosure spoken first | Live, twice, ConversationRelay, agent mode, to the owner's own phone. Call 1 (80 s, owner call 1): disclosure, first reply in 1.56 s, then about 39 s of silence and the owner hung up. Call 2 after the fix (43 s, owner call 2): no silence; first text of Hermes's turns after 2.0, 1.6 and 7.3 s, with spoken fillers covering the slow turn; Hermes finished the call itself. | evidence/trial/call_68921278eab742c6/trial.json, evidence/trial/call_9fda11b071df4fea/trial.json, STATUS.md (2026-11-05 18:52Z) |
| Answering a call to your own forwarded line | Live, twice, both with the owner calling his own Google Fi number, forwarded by Fi "Always forward" to the Twilio line. Call 3 (92 s, owner call 3): Hermes answered with the disclosure; Twilio received the original caller in From and no ForwardedFrom; the plugin wrongly treated the owner as a hidden caller. Call 5 (121 s, owner call 5): the owner's number was recognised, Fi's screening prompt ("press 1 to take this call") was answered with 1, and Hermes held a 2-minute conversation without tools beyond the call. It offered to take a note five times; L1 round 16 limits that to once (offline). No PIN step on either call. | evidence/trial/call_c20d76d32e9e47db/trial.json, evidence/trial/call_94909a575a8941db/trial.json, docs/lanes/L1.md sections 21 and 27 |
| Verified caller ID (your mobile number shown to the business) | Live. Twilio verified the owner's Google Fi number as caller ID; calls 1 and 2 used it. | evidence/twilio/04-verify-check.json, docs/briefs/L1-round7-live-findings.md |
| Calling a real business (Delta Air Lines reservations, toll-free, approved by the owner) | Live, four times; no person reached on any run. Run 1 (68 s, Delta run 1): Delta answered with a conversational automated assistant; the menu gate kept Hermes silent, the task ended as failed, and the summary named phrases the gate had blocked, so they were never spoken. Run 2 (57 s, Delta run 2): the bot's cut-off self-introduction led to a false "person" verdict; the owner was rung for 18 s and did not pick up; the fallback question went to the bot and its non-answer was reported as an answer. Fixes since then, offline: fixed phrases for automated assistants (L1 round 25), a clear yes to our live-person question before any ring, one quiet gate for every fixed line, and honest fallback outcomes (rounds 30-31). Run 3 (a first attempt was refused by the quiet-hours gate, no call placed; the retry, 14 min 3 s, Delta run 3 retry): the assistant transferred the call to Delta's real hold queue ("The current phone wait time is 23 minutes"); Hermes then spoke into the hold queue seven times (two live-person questions, five "Agent."/"Representative." phrases); the call ended when the trial's line service was stopped (end reason service_restart). Fixed offline in L1 rounds 39-41 and L2 round 10: no phrases while on hold. Run 4 (44 min 43 s, Delta run 4): one "Agent." to the assistant, then Delta's real hold queue (45 minutes announced); 56 announcements classed as hold messages, 0 phrases spoken into hold, no false "person" verdict, ended at the trial's maximum call length with no person reached. | evidence/trial/call_b669b105d4774a49/trial.json, evidence/trial/call_2243b7f205194ab5/trial.json, evidence/trial/delta3-stdout.jsonl, evidence/trial/call_05899c72ff3649bf/trial.json, evidence/trial/call_d4c0d44472b144c5/trial.json, docs/lanes/L1.md sections 38, 42, 43, 51-53 |
| Phone menus (keys and speech) | Offline tests with a fake Twilio. Not tried on a real keypad menu; on Delta's conversational assistant one fixed "Agent." got a transfer to the hold queue (run 4). | tests/l1/test_flows.py, docs/lanes/L1.md section 1 |
| Waiting on hold | Live on a real hold queue, twice (Delta runs 3 and 4). Run 4 waited on Delta's hold queue for about 44 minutes in silence: 0 phrases spoken into hold, ended at the trial's maximum call length; no agent picked up, so the hand-off at the end of a hold is still unproven live. Run 3 (14 min 3 s) talked into the hold queue; fixed before run 4. Offline: a 40-minute virtual hold with 0.0 s of voice-model time. | evidence/trial/call_d4c0d44472b144c5/trial.json, evidence/trial/call_05899c72ff3649bf/trial.json, docs/lanes/L5.md section 4, tests/l5/ |
| Detecting that a person answered | Experimental. On authored transcripts plus the two live replies: 0 false "person" results on 31 held-out non-human cases; 2 of 7 held-out human cases missed with a model, 4 of 7 without one. The live reply to the live-person question ("Yeah. It's me, ...") is now recognised as a person; this is not yet confirmed on a new live call. Authored airline hold transcripts: 0 false "person" in 13 held-out non-human events, 0 of 2 agents missed. Live, Delta run 2 produced a false "person" from a bot; replayed through the current detector it gives 0 of 9 "person" verdicts (offline). Delta runs 3 and 4 (about 58 minutes of real assistant and hold audio): no "person" verdict and no owner ring; no real person came on, so detecting a real agent is still unproven live. | docs/lanes/L2.md rounds 2, 4 and 9, STATUS.md (L2 r9), tests/l2/round2-after.json, tests/l2/round4-airline-metrics.json |
| Ringing you and bridging you in | The owner ring has happened live once, wrongly (Delta run 2, no pickup). A bridge has not happened live. Offline tests with a fake Twilio, including a full end-to-end fake call over the real ingress, and the opt-in live-leg bridge (owner_live_bridge). | evidence/trial/call_2243b7f205194ab5/trial.json, tests/l1/test_flows.py, docs/lanes/L1.md sections 12 and 26 |
| Callbacks from a business | Offline tests: matching a callback to its task, callbacks forwarded from your own line (for example Fi), unknown callers (message, or ring you and you press 1). A real business callback has not happened yet (call 3 was the owner). | tests/l1/test_r7_callbacks.py, docs/lanes/L1.md section 16 |
| Reading SMS codes from an Android phone | Live read on a Pixel 10 Pro XL (Android 17): one message with a code was found, the code masked in the log. Whether Android 17 delays fresh codes is not measured yet: the probe found no phone attached. | evidence/sms/probe-check.txt, evidence/sms/probe-inbox.txt, evidence/sms/otp-delay-20261105T184714Z.json |
| Codes sent to a Twilio number | Live observation: Twilio filtered a one-time-code text to this project's Twilio line (error 30038). Codes can only come from your carrier phone. | STATUS.md (Vezir, 2026-11-05) |
| Sending an approved code into a call | Offline tests only, including the real Hermes core in a throwaway home. Off in the live trial. | docs/lanes/L4.md sections 2 and 5 |
| Grok Voice path (optional xAI key) | Live, once. Call 4 (64 s, owner call 4, to the owner's phone): Grok spoke for Hermes and used three tools (one question to Hermes, the outcome report, the hang-up); the conversation session connected in 1214 ms once the person was detected, and voice_first_audio logged 490 ms; Grok was connected 53 s and hold-path speech-to-text 12.8 s; harness estimate $0.096 of xAI time (not an invoice). Seen on that call and fixed offline in L5 round 6: the disclosure recorded twice in the transcript (spoken once) and repeated copies of the caller's words. No barge-in happened, so barge-in is still unproven live. Key tones on this path are untested. | evidence/trial/call_374a992b53c044f9/trial.json (grok_evidence), docs/lanes/L5.md section 7 |
| Owner line mode (call your line, key a PIN, have Hermes call a business) | Experimental, off by default (off unless HERMES_LINE_OWNER_PIN is set). Offline tests only; no live call has used the PIN yet (calls 3 and 5 had no PIN step). Security review R1b delta 35: YES for one attended, keypad-only trial in which the owner asks Hermes to call his own other line; not cleared for business calls or a broader allowlist. Two Medium limits remain (see setup step 8). | docs/lanes/L1.md sections 22-26, evidence/reviews/R1-DELTA35.md |
| Desktop pane | Runs inside a real Hermes Desktop (v0.22.5, isolated sandbox install, real REST API) with sample data, matching the offline harness renders. Replayed in real time with the recorded data of Delta run 2: the pane went from "Dialing" straight to the owner ring, because "A person answered" lasted 1.2 s and the pane polls every 5 s; the ended headline showed the summary's status line; the time strip was wrong for real data (backlog B7-B9, B12; fixes briefed). Not yet captured during a live call. | docs/lanes/L3.md rounds 5 and 6, tools/pane-preview/screenshots/desktop/, tools/pane-preview/screenshots/live/replay-call_2243b7f205194ab5/, docs/BACKLOG.md |
| hermes plugins validate --install-deps | Passed, exit 0 (cold run). No plugin warnings up to L1 round 15; since the call-profile check (2026-11-10) one warning, private core imports, for the four guarded imports listed in docs/release/PR.md. | tests/l1/evidence/validate.txt (round 15) |
| Offline test suites | 1247 tests pass (L1 535, L2 130, L3 66, L4 317, L5 88, L6 111), no network. | docs/release/test-run.txt |
How a call goes
- You ask in chat: "Call Sample Bistro at +1 775 555 0100 and book a table for 4 at 7, put me on if they need me."
- Hermes calls
hermes_line_request_call. Nothing is dialed. The call waits until you approve it in chat with
/line approve <task_id>(the Desktop pane shows it as waiting). The approval message shows the whole goal the
call's Hermes session will get, in numbered parts when it is long; the other party may hear any of it. If that
message cannot be posted, the request is withdrawn. Hermes cannot approve its own call, and nothing said on the
phone counts as approval. - Hermes dials, says the disclosure line ("Hi, this is Hermes, an AI assistant calling on behalf of ..."),
presses menu keys, and stays quiet on hold. If the business answers with a conversational automated assistant,
Hermes says only fixed phrases the line picks ("Representative.", "Agent.", "Speak to an agent." and a few more,
see Limits), never model text. - When the transcript shows a person, Hermes asks "Am I speaking with a live person?". On a restricted task your
phone rings only after a plain yes to that question, never on a guess. Every fixed line Hermes says after the
disclosure waits until the business has finished and 1.2 seconds of quiet have passed (10 seconds at most). - Then, as you chose in the request:
- owner mode: your phone rings with a short private message; you press 1 and are joined to the business.
- agent mode: Hermes talks to the person and finishes the task with the title and goal you approved. The
call runs in its own Hermes profile, without your memory, user profile, persona or other tools (setup step 4;
Limits lists what is not closed).
- When the call ends, a summary goes back to the chat that asked for it, also after a restart. One-time codes and
card numbers are masked.
The pane shows the live state, the time on hold, the masked transcript, and Take over, Say this and
Hang up buttons.
Requirements
- Hermes 0.22.5 or newer, with the Hermes gateway able to run on the same machine. Every call is a Hermes session
run throughhermes acp(docs/lanes/L1.mdsection 8). - A Twilio account with a US local phone number that can make and receive voice calls.
- A public HTTPS address that forwards to the plugin's local port (a tunnel).
- Optional: an xAI API key, for the Grok Voice path.
- Optional: an Android phone with
adbinstalled, on USB or on Wi-Fi (wireless debugging), to read SMS verification codes.
Only US numbers can be called: geographic and toll-free (see Limits).
Setup
1. Twilio account, number and API key
- In your Twilio account, buy a US local number with voice. This is the line number: callbacks arrive here.
Verification codes cannot be read from it: Twilio filters one-time-code texts sent to its numbers (error 30038,
"OTP Message Body Filtered", body redacted; seen on this project's line on 2026-11-05). Codes are read from your
own carrier phone instead (step 6). - Create a Twilio API key (SID starts with
SK) and keep its secret. - When Hermes installs the plugin, it asks for the three required secrets declared in
plugin/plugin.yaml
(requires_env). The plugin reads them only through Hermes (agent.secret_scope.get_secret), never from files.
| Name | What |
|---|---|
| HERMES_LINE_TWILIO_ACCOUNT_SID | Your Twilio Account SID (AC...) |
| HERMES_LINE_TWILIO_API_KEY_SID | The API key SID (SK...) |
| HERMES_LINE_TWILIO_API_KEY_SECRET | The API key secret |
| HERMES_LINE_TWILIO_AUTH_TOKEN | Optional, recommended. With it, every Twilio webhook is checked by Twilio's signature. Without it, the plugin uses a secret URL path plus a Twilio API lookup of every call leg; that fallback and its limits are described in docs/lanes/L1.md section 9. |
| XAI_API_KEY | Optional. See step 5. |
| HERMES_LINE_OWNER_PIN | Optional, 4 to 8 digits. Turns on owner line mode (step 8). Stored only in Hermes's secret store; never shown to Hermes or written to any log. |
2. Caller ID
from_number is required. Set it to your Twilio number, or, to show your own mobile number to the business,
verify that number in Twilio as an outgoing caller ID first (Twilio calls that phone and you type the code it
shows) and set from_number to it.
3. Public address
Twilio has to reach the plugin over HTTPS and WSS. The plugin listens on 127.0.0.1:8788. Run a tunnel to that
port and put its https:// origin in public_base_url. The live trial used a Cloudflare quick tunnel
(docs/lanes/L6.md). A quick tunnel gets a new hostname each time, which is fine for outbound calls; callbacks
need a stable hostname (a named tunnel on your own domain, for example).
4. Settings
Settings live under plugins.entries.hermes-line.settings in your Hermes config.yaml. A minimal setup:
plugins:
entries:
hermes-line:
settings:
enabled: true
public_base_url: "https://line.example.com"
from_number: "+17755550100" # your Twilio number, or a verified caller ID
line_number: "+17755550100" # your Twilio number
owner_number: "+17755550111" # your own phone, rung to bridge you in
owner_name: "Sam"
policy:
owner_timezone: "America/Denver"
allowed_destinations:
- "+17755550111" # your own phone must be here too
- "+17755550199" # each business you want Hermes to be able to call
(The numbers above are placeholders.)
A Hermes profile for the calls (required). A stranger on the phone partly drives the call's Hermes session, so
that session may hold only the call tools (never terminal, files, browser, web or other MCP tools) and none of your
memory, user profile or persona. Hermes bounds a session's tools and memory per profile, so create a new profile for
calls. Do not use --clone, --clone-all or --clone-from: they copy your SOUL.md, MEMORY.md, USER.md,
.env and settings. Then set only its model and provider:
hermes profile create hermes-line-calls
hermes --profile hermes-line-calls model # model, provider and the provider's key, kept in this profile
The provider key must be in the call profile: the plugin does not pass the keys in your shell or your Hermes
process to hermes acp (see the environment below).
Add to ~/.hermes/profiles/hermes-line-calls/config.yaml:
platform_toolsets:
acp: []
memory:
memory_enabled: false
user_profile_enabled: false
plugins:
enabled: []
Leave these unset or empty: memory.provider, display.personality (or a neutral name: none, default,
neutral), agent.system_prompt, personalities, agent.personalities, prefill_messages_file,
agent.prefill_messages_file, agent.environment_hint, agent.coding_instructions, platform_hints, hooks and
mcp_servers. Because the profile is created fresh, its .env holds only the provider key that
hermes --profile hermes-line-calls model writes; keep it that way. Hermes itself loads that .env into the call
agent, and the plugin does not inspect it, so anything else put there reaches the call session (Limits). The
profile's hooks/ folder must have no hook directories. Keep the SOUL.md Hermes wrote into the new profile (its stock text), or empty it. Then set
call_profile: hermes-line-calls in the plugin settings. The plugin does not need to be installed in that
profile.
What the plugin does with that profile:
- Every call session starts with Hermes's own
--ignore-rules(HERMES_IGNORE_RULES=1forhermes acp): Hermes
loads noSOUL.md, no project context file (AGENTS.md,.cursorrulesand the like), no memory and no preloaded
skills into it. Hermes loads the profile's.envover the environment the plugin gives it (andhermes acp
loads it again after its command-line flags), so aHERMES_IGNORE_RULES=0there would undo it; the plugin does
not inspect the.env, which is why the setup keeps only the provider key there (Limits). - The plugin checks the profile when you approve a call, when an inbound call arrives, and again just before the
session starts. It refuses the profile if memory or the user profile is on, a memory provider or any key listed
above is set, itshooks/folder has a hook directory, a plugin is enabled (or one is installed while
plugins.enabledis unset),memories/MEMORY.mdormemories/USER.mdis not empty,SOUL.mdis neither empty
nor Hermes's stock or earlier auto-seeded text. It readsconfig.yamlandSOUL.mdand lists thepluginsand
hooksfolders; the memory files are only checked for size. It does not inspect the profile's.env. It is a
fixed list, not a check of everything Hermes loads (Limits). - With a refused profile, nothing is dialled. The
/line approvereply names each problem and the fix, and the task
is marked failed with the outcomeblocked: call profile not neutral: ...; ask Hermes for the call again after the
fix. An inbound caller hears "Sorry, this line cannot take calls right now. Goodbye." and the call ends, on both
voice paths. When the service starts with a refused profile, the plugin says so once: in the chat of the latest
task that came from a chat (it looks at the last 50 tasks), or, when none did, in reply to your next/line
command orhermes_line_request_call. - If you made this profile earlier with
--clone, delete and recreate it:hermes profile delete hermes-line-calls,hermes profile create hermes-line-calls,hermes --profile hermes-line-calls model, then the
config lines above. The check refuses a clone for the memory, user profile,SOUL.mdand settings it copies, and
the/line approvereply says to delete and recreate it. Editing the clone until it passes is not enough: it keeps
your.env(provider keys, platform tokens) and your skills, which the plugin does not inspect. - The plugin then reads the session's tool list; if it holds anything beyond the call tools, nothing is dialled and
the event log says why. - The session runs in a new, empty, owner-only directory, made for the call and removed when the call ends (also
when the session fails to start); never in the gateway's working directory. It is made under the first of the
process temp directory (TMPDIR),/tmpand/var/tmpthat has no.gitand none of the project files Hermes
looks for (Makefile,package.json,pyproject.toml,AGENTS.mdand the like) in it or any folder above it.
With none of them clean, no call session starts. Inside a git repository or project, Hermes would add that
project's root, branch, status and recent commit subjects to the session's prompt. hermes acpgets only these variables from the plugin's environment:PATH,HOME,USER,LOGNAME,LANG,
LC_ALL,LC_CTYPE,LC_MESSAGES,TMPDIR,HERMES_HOME,XDG_CONFIG_HOME,XDG_DATA_HOME,XDG_STATE_HOME,
XDG_CACHE_HOME,XDG_RUNTIME_DIR,SYSTEMROOT,SSL_CERT_FILE,SSL_CERT_DIR,REQUESTS_CA_BUNDLE,
CURL_CA_BUNDLEand the proxy variables (HTTP_PROXY,HTTPS_PROXY,NO_PROXY,ALL_PROXY, also lower case),
plusHERMES_IGNORE_RULES=1. When the call profile's gateway is not running,hermes acpstarts it with that
environment.
In Hermes Desktop, turn Hermes Line on in Capabilities → Plugins, then reload the window (⌘R). In testing, switching
it off and on without a reload did not load the pane (docs/lanes/L3.md round 5).
Then check the service with /line service status. It reports whether the
service is running and whether webhooks are checked by signature or by the fallback.
To check the whole setup without placing a call, run hermes hermes-line check (or /line check; Quick start,
step 6).
5. Grok Voice (optional)
With XAI_API_KEY set, the default voice.transport: auto uses Twilio Media Streams and xAI's Grok Voice for the
spoken conversation, and xAI's streaming speech-to-text while on hold. Without it, calls use Twilio
ConversationRelay, where Twilio does speech-to-text and text-to-speech and your Hermes writes every sentence.
Force one with voice.transport: conversation_relay or media_streams. This path is tested offline and with one
real xAI connection that set up a session without audio (tests/l6/dry-round6-grok.json), and one live 64-second call
to the owner's phone (evidence/trial/call_374a992b53c044f9/trial.json).
6. SMS codes from an Android phone (optional)
Codes must arrive on a real carrier SIM. Twilio numbers do not receive one-time-code texts (error 30038), so
never give a service the Twilio line number for verification.
Connect the phone by USB with USB debugging on and adb on your PATH. The plugin reads the SMS inbox with
adb shell content query (read-only). Set sms_source: none to turn this off. Set sms_adb_serial if more than
one device is attached.
Wireless debugging (no cable). The phone can stay on Wi-Fi instead of USB:
- On the phone: Developer options, Wireless debugging on, then "Pair device with pairing code". It shows an
ip:portand a six-digit code. - On the computer:
adb pair <ip:port>and type the code. Pairing is done once. - Find the phone's service name:
adb mdns serviceslistsadb-<id>-<suffix>with_adb-tls-connect._tcp.
(adb devicesshows the same name with._adb-tls-connect._tcpappended once it is connected.) - Set
sms_adb_serialto that name, for exampleadb-5B050DLCQ000P3-eQkDjp. The plugin then always talks to
that phone and never to "whichever device is attached". It also acceptsip:port, but the port changes every
time wireless debugging restarts, so the service name is the better choice.
If the link drops, the plugin makes one bounded attempt (10 seconds) to find the phone again with adb mdns services and adb connect, then retries the read once. It never returns an empty result on failure: you get
"phone unreachable over Wi-Fi (asleep or off the network)", "paired device not advertised on the network", or
"device unauthorized" (accept the wireless debugging prompt on the phone, or pair again). A sleeping phone, a
Wi-Fi change or switching wireless debugging off drops the link. Reading one code takes about 3 to 6 seconds over
Wi-Fi (the phone starts a small Java program for every read), occasionally 10 seconds or more. USB is more
reliable and is recommended when the phone stays next to the computer; use Wi-Fi when it cannot.
To let a code into a call, run /line allow-code <task_id> <sender> call. You see the call and the number the
code will go to, then confirm with /line confirm-code .... Hermes then calls line_otp_to_call; the code is
sent as key tones or spoken by the plugin. The model never sees the code. Transcripts, events, the pane and
logs show only [verification code sent].
Codes go only into the approved live call. This build does not type codes into web pages.
7. Callbacks
Callbacks need the Twilio line number's voice webhook pointed at the plugin, a stable public hostname, and the
service running all the time. A callback is matched to its task by the caller's number; an unknown caller hears the
AI disclosure and leaves a message, or asks for you and your phone rings (inbound_ring_owner). If businesses see
your own mobile number (a verified caller ID), forward that line to the Twilio number and set forwarding_number.
For a Google Fi line the steps are in docs/release/fi-forwarding.md.
Callbacks are tested offline (tests/l1/test_r7_callbacks.py), and the plugin does not yet print the webhook
address to set on the Twilio number. On both live forwarded calls so far (Google Fi "always forward"), Twilio got
the original caller's number and no ForwardedFrom, so the plugin records the forwarding as unknown rather than
guessing. Other carriers are untested.
- Carrier screening. A forwarded call can open with the carrier's own prompt before the caller is connected.
Google Fi says "In case of a fast pickup… you need to press 1 to take this call." Withforwarding_numberset,
the greeting waits until the caller speaks, or up toinbound_screening_hold_s(6 s). If that prompt is heard in
the first 15 seconds, the line presses 1 at once and then greets the caller. Only that prompt counts. A caller who
phones the Twilio number directly hears the greeting as soon as they say hello, or after the wait. - Notes. On a call, Hermes offers to take a note at most once. A later offer is not said unless the caller asks
for a note or a message.
8. Owner line mode (optional, experimental, off by default)
Call your Hermes Line from the road and have Hermes phone a business for you. Set HERMES_LINE_OWNER_PIN (4 to 8
digits) and owner_number.
Status: experimental, tested offline only. The latest security review, R1b delta 35
(evidence/reviews/R1-DELTA35.md), cleared one attended, keypad-only trial in which the owner asks Hermes to call
his own other line, with a two-number allowlist; it is not clearance for business calls or a broader allowlist. It
names two Medium limits: the keypad confirmation does not identify a replacement goal or prove the read-back was
heard, and a PIN spoken slowly in fragments can reach Hermes before it is scrubbed. Key your PIN; never say it aloud.
- Caller ID is not proof. A call from
owner_numberis only offered the PIN: "If you are Can, key your PIN,
then press pound." Until the PIN passes, the caller is an unknown caller who can leave a message, nothing more. - Entering the PIN. Keypad only: key it and press
#(or pause). A spoken PIN never counts. If the PIN is said
aloud anyway, even split over several sentences or with pauses and punctuation, the plugin replaces every piece
with[PIN]before the transcript or Hermes sees it. Speech that ends in what could be the start of your PIN
waits up to 1.5 seconds for the rest. The PIN is never passed to Hermes and never appears in transcripts, events or
logs. - Lockout. Wrong PINs count against the line, not the caller, and survive redials and restarts.
- After 3 wrong tries Hermes says goodbye and hangs up, with no hint, and owner line mode is locked for 15 minutes.
- After 10 wrong tries in 24 hours it stays locked until you run
/line unlock-ownerin chat. - Each lockout sends you one chat message. While locked, the PIN is not offered and keyed digits are not checked.
- After the PIN. Ask questions, or say "call Sample Bistro for me" (and "ring me when they answer" if you want
to talk to them yourself).- The line reads the number back and ends with "To confirm, key the last four digits of the number, then pound."
Keying those digits and#within 60 seconds approves that one call to that number, and nothing else. The line
asks for the last four digits only for your own number or a number you already approved before. Any other number
needs its last seven digits, so two numbers that end the same cannot be confused. - Saying yes never approves anything on this line. Wrong digits, or no digits within 60 seconds, cancel the
confirmation; the task then waits for your approval in chat (/line approve). - A second call in the same conversation needs its own confirmation.
- The call is dialled when you hang up, because the line runs one call at a time. A call you asked for on the
phone has no chat behind it, so its summary is not posted to a chat: it is in the Desktop pane and in the call
status Hermes reads (hermes_line_call_status). - Staying on the line (
owner_live_bridge, off by default). With this setting on, a call where you asked to
be put through is dialled at once while you stay on your call. When a person answers, the line moves your call
and theirs into one conference; your phone is not rung a second time. Until then you can keep talking with
Hermes, and pressing star cancels the call. If nobody answers, or the call ends first, Hermes tells you on your
call. If you hang up first, the call carries on and your phone rings when a person answers, as without the
setting. Once you are connected, the conference ends when either side hangs up. Only one such call runs at a
time, and no other call can start while it runs. It needs the same approvals as any call; only the second
call to your phone is skipped. Tested offline only.
- The line reads the number back and ends with "To confirm, key the last four digits of the number, then pound."
- What a phone session can't do. It runs on the restricted call profile (step 4), so it has no tool for your
memory, calendar, files or anything else, also on your own calls to the line (owner line mode). Of what you asked
for, it has the task's title and goal and your configured name. It says so, and you can ask in chat. - Stated limits of the call session's isolation (what the plugin does not close):
- Your machine. Hermes adds a runtime block to every session, the call session included: the host OS, the home
and working directory paths (the home path often holds your account name), the profile name, the time zone and,
unlessagent.environment_probeis false in the call profile, a one-line probe of the host. A caller who asks
may hear it. - Callbacks matched by caller ID. Caller ID can be faked. A caller who fakes the number of a business you asked
Hermes to call is greeted with that task's title, and the call session gets that task's title and goal. - A running call gateway. The environment allow-list applies when
hermes acpstarts the call profile's
gateway. A gateway that already serves the call profile (started by you, or a multiplexed gateway that serves
all your profiles) keeps the environment it was started with, and the call session runs there. - The call profile's
.env. Hermes itself loads it into the call agent and its gateway; the plugin does not
open or inspect it. The setup creates the profile fresh, so the file holds only the provider key that
hermes --profile hermes-line-calls modelwrites. Anything added there reaches the call session as set: a
launch flag such asHERMES_IGNORE_RULES=0would undo--ignore-rules, and a prompt variable such as
HERMES_ENVIRONMENT_HINTwould add text to the prompt. A profile made earlier with--clonekeeps your own
.env: recreate it (the/line approvereply says so). - What the check does not read. It does not read the skills, or keys of
config.yamlthat are not listed in
setup step 4. Recreate a cloned profile instead of editing it. - Managed configuration. The check reads the profile's own
config.yaml. A managed (administrator) layer that
Hermes applies on top of it, including a managed.env, is not read, and could set a listed key. - Prompt text in a running gateway. A call gateway that is already running keeps the environment it was
started with; prompt variables set there (HERMES_EPHEMERAL_SYSTEM_PROMPT,HERMES_ENVIRONMENT_HINTand the
like) reach the call session's prompt. The plugin cannot see that environment. - Your own Hermes speaking on the call.
hermes_line_sayfrom the owner's own agent has no per-sentence
approval: your Hermes, which has your memory and tools, can make the call say any sentence, and it reads the
other party's words throughhermes_line_call_status. The tool's description tells it to say only sentences you
asked for, never one the call's transcript asks for. Hermes's plugin API asks for a choice only from a slash
command, not from a tool call, so a per-sentence approval would need a new approval route./line sayand the
Desktop "Say this" are you. - Long goals on narrow chats. The approval request is split into parts of at most 1,500 characters, under the
smallest limit of Hermes's built-in chat adapters (SMS, 1,600). A chat behind Hermes's relay with a lower
negotiated limit can still cut a part. The Desktop REST snapshot (GET /calls) carries each task's goal as
task_goal; the pane has no approve button. - Text from the call in your chat. The summary posted to your chat, and what
hermes_line_call_statusreturns
to your Hermes (masked transcript tail and summary), contain the other party's words. The posted summary is
marked as untrusted. The plugin also tries to tell your Hermes session that the call ended and its task status
(one sentence, no words from the call); Hermes refuses that unlessallow_gateway_injection: trueis set for
the plugin (plugins.entries.hermes-line), and the posted summary does not depend on it. - The check is a snapshot. It runs at approval, at admission and before the session starts; a change to the
profile in between is not seen until the next check. Only you can write the profile.
- Your machine. Hermes adds a runtime block to every session, the call session included: the host OS, the home
- Owner line mode works on ConversationRelay calls; the Grok Voice path has no PIN step yet.
- Your PIN's key tones pass through Twilio like any keypad input.
Commands and tools
/line commands (tools and the model cannot run approve, deny or allow-code). The plugin does not check who sends
them: any sender your Hermes gateway admits can run every /line command, including /line approve. In a group
chat that is every admitted member, so run the plugin on a gateway that admits only you.
/line status
/line approve <task_id> /line deny <task_id>
/line takeover <call_id> /line hangup <call_id> /line say <call_id> <text>
/line allow-code <task_id> <sender> call then /line confirm-code <nonce>
/line unlock-owner (after a PIN lockout, step 8)
/line service start|stop|status
/line check (setup check: no call, read-only Twilio requests)
Tools the agent gets: hermes_line_request_call, hermes_line_call_status, hermes_line_list_tasks,
hermes_line_say, hermes_line_hang_up, hermes_line_take_over; during a call hermes_line_call_press_digits,
hermes_line_call_finish, hermes_line_call_bring_owner, and for owner line mode hermes_line_call_new_task
(refused unless the caller passed the PIN); for codes line_otp_to_call. The bundled skill hermes-line tells the agent when and how to use them.
Settings
| Setting | Default | What |
|---|---|---|
| enabled | false | Run the call service in this Hermes process. Nothing starts without it. |
| public_base_url | empty | Your tunnel's https:// origin. |
| listen_host, listen_port | 127.0.0.1, 8788 | Where the plugin listens for Twilio (through the tunnel). |
| from_number | empty | Caller ID for outbound calls (Twilio number or verified caller ID). |
| line_number | empty | Your Twilio number for callbacks. |
| owner_number | empty | Your phone, rung to bridge you in. Must be on the allowlist. |
| owner_name | the owner | Used in the disclosure line. |
| disclosure_text | "Hi, this is Hermes, an AI assistant calling on behalf of {owner}. This call is not recorded." | First line of every outbound call. |
| ring_timeout_s / owner_ring_timeout_s | 30 / 25 | How long the business / your phone may ring. |
| max_call_minutes | 90 | Hard limit per call (1 to 240); also sent to Twilio as the call time limit. |
| callback_window_hours | 72 | How long a task waits for a callback. |
| ingress_auth | auto | auto: signatures if the Auth Token is set, else the fallback. signature: refuse to start without the Auth Token. |
| sms_source | adb | adb or none. |
| sms_adb_serial | empty | Which phone to read: a USB serial, the wireless-debugging service name (adb-<id>-<suffix>), or ip:port. Empty means the one attached USB device. |
| call_profile | empty | The Hermes profile the call sessions run in. It must give ACP no toolsets (see Setup, step 4); otherwise no call is dialled. |
| forwarding_number | empty | Your own line (for example Google Fi) that forwards calls to line_number, so callbacks to it reach Hermes. |
| inbound_ring_owner | true | Let an unknown inbound caller ask for you; your phone rings and you press 1 to join. |
| inbound_screening_hold_s | 6 | With forwarding_number set: how long an inbound greeting waits for a carrier screening prompt before it plays (0 to 10 s, 0 is off). |
| owner_live_bridge | false | Owner line mode (step 8): dial a call you asked to be put through while you stay on your call, and join your call to it when a person answers. Off: it is dialled after you hang up and rings you back. |
| reply_budget_s | 2.5 | Longest silence a person on the line hears before Hermes says something (0.5 to 3 s). |
| voice | see ADR 0004 | transport (auto, media_streams, conversation_relay), conversation (grok or hermes_text, where Grok only reads out what Hermes wrote), timings. |
| policy.allowed_destinations | empty | With the allowlist on, the only numbers that can be dialed. Empty means no call can be placed. |
| policy.allowlist_only | true | Dial only numbers on allowed_destinations. Set false to let Hermes request calls to any supported US number; each call still needs your approval. |
| policy.blocked_destinations | empty | Numbers that are never dialed, even if allowed (for example a number of your own that runs something else). |
| policy.disclosure_enabled | true | Speak the disclosure line. |
| policy.quiet_hours_enabled, quiet_hours_start, quiet_hours_end | true, 21:00, 08:00 | No outbound calls in this window; callbacks never ring you in it. |
| policy.owner_timezone | America/Los_Angeles | Set yours. Quiet hours use it. |
Fixed, not settings: every call needs your approval, and one call at a time per line
(plugin/line/policy/settings.py refuses any other value).
Disclosures
What you should know before installing:
- The other party hears an AI. Every outbound call starts with the disclosure line unless you turn
policy.disclosure_enabledoff. Hermes is instructed never to claim to be human. - No recording. The plugin asks Twilio not to record (
Record=falseon every call it places). In 0.1.0 there is
no recording feature at all;policy.recording_enabledexists but nothing reads it. - Allowlist and approval. By default only numbers you list can be dialed (
policy.allowlist_only), and each
call needs your approval with/line approve, also when the allowlist is off. The model, tools and anything said
on a call cannot approve a call or a code. Numbers inpolicy.blocked_destinationsare never dialed. - Emergency numbers are blocked. Only full US numbers in
+1and ten digits can be dialed: geographic
numbers and toll-free (800, 833, 844, 855, 866, 877, 888). 911 and other emergency numbers, short codes such as
211, 311 or 411, N11 area codes and exchanges, premium numbers (900, 976) and non-US numbers are always refused,
before approval and again before dialing; no setting, allowlist entry or approval overrides that. 57 refused
input shapes are tested (docs/lanes/L2.md). Do not rely on Hermes Line for emergencies. - Codes need consent. An SMS code is released only for a task you approved, from the sender and time window
you approved, to the one live call you approved, once. - Third-party services and what they receive:
- Twilio (
api.twilio.com): call control, and all call audio passes through Twilio. On the ConversationRelay
path Twilio also transcribes the call and speaks Hermes's text. The setup check (/line check) sends only
read-only GET requests: your account's numbers filtered to the configured ones, and its verified caller IDs. - xAI (
api.x.ai), only withXAI_API_KEY: hold and menu audio goes to xAI speech-to-text, and the
conversation audio goes to Grok Voice. xAI's retention was not checked. - Your tunnel provider: carries Twilio's webhooks and audio streams to your machine. Webhook URLs contain a
secret path; treat those URLs and Twilio's request logs as sensitive. - Your Hermes model provider: what the business says (with codes and card numbers masked) is sent to your Hermes
model as a normal Hermes session.
- Twilio (
- The call's Hermes session holds only the call tools. Each call is a Hermes session through
hermes acp --profile <call_profile>. Before dialling, the plugin reads that session's tool list and refuses the
call unless it holds only the call tools (press keys, finish, bring the owner in, and, only after the owner's PIN,
request a new call) and Hermes's deferred-tool bridge. The session never inherits YOLO, ignore-rules or pre-accepted hooks from the plugin's environment, and
the call driver declines every permission request. What the other party says is untrusted input to that session,
and the call briefing and the bundled skill tell Hermes so. - The call's Hermes session gets none of your memory, user profile or persona. It starts with Hermes's
--ignore-rules, and the plugin refuses a call profile that has memory, a user profile, a memory provider, one of
the persona or prompt keys listed in setup step 4, MCP servers, hooks (inconfig.yamlor thehooks/folder),
enabled plugins, non-empty memory files or aSOUL.mdother than Hermes's stock text (setup step 4). It reads the
profile'sconfig.yamlandSOUL.mdfor this and lists itspluginsandhooksfolders; the memory files are
only checked for size. It does not inspect the profile's.env, which Hermes itself loads into the call agent;
the setup keeps only the provider key there (Limits). The session runs in an
empty temporary directory outside any git repository or project, and itshermes acpprocess gets an exact
environment allow-list (setup step 4). Of what you asked for, it gets the task's title and goal, which the
approval message shows you in full, and your configured name. See Limits for what is not closed. - Call summaries. The summary quotes the other party. It is posted to the chat that asked for the call (into
that stored session, also after a restart), marked as untrusted text from the call; it is not injected into your
Hermes session. The plugin tries to tell that session only that the call ended and its task status; Hermes
refuses it unlessallow_gateway_injectionis on for the plugin (Limits). - Reads outside the plugin's own data: the SMS inbox of the attached Android phone (through
adb), when
sms_source: adb. At approval, at inbound admission and before each call session: the call profile's
config.yamlandSOUL.md, the lists of itspluginsandhooksfolders and the size of its memory files (not
its.env); then the call session's tool list (fromhermes acp). Before each call session:
whether a.gitor a project file is in the temp directory or a folder above it. - Programs it starts:
adb(read-only queries; it gets onlyPATH,HOME,LANGandANDROID_*/ADB_*
from the environment), and onehermes acpprocess per call, which starts the call profile's Hermes gateway
(idle-exit) if it is not already running. The setup check (/line check) runshermes --version(thehermesnext to
its Python, then the one onPATHif the first gives no version), with onlyPATH,HOME,HERMES_HOME,LANG
andTMPDIR, and pings the gateway's own loop-tick socket for one byte. - Background service: with
enabled: true, a call service runs inside the Hermes process. It listens on
127.0.0.1:8788for Twilio (through your tunnel) and on a second, loopback-only port that serves the four in-call
tools to the call's Hermes session with a per-call token. - Stored data:
plugin-data/hermes-line/line.db(tasks, calls, masked transcripts, events, SHA-256 hashes of
released code messages, owner-PIN failure counts and lockouts, never the PIN) andplugin-data/hermes-line/ingress-path.secret(mode 0600). The plugin stores no
credentials; Hermes holds them. Hermes itself saves each call session (masked text) in its own history. - No telemetry. The plugin reports nothing to its authors.
Costs per call
You pay Twilio (and xAI, if used) directly. Hermes model use counts against your existing Hermes setup and is not
priced here. List prices and calculations from docs/adr/0004-realtime-voice.md and
docs/spikes/S2-telephony.md (read 2026-11-04 and 2026-11-05); taxes, rounding and account-specific fees were not
checked.
| Call | Grok Voice path | ConversationRelay path |
|---|---|---|
| 40 min on hold, then 5 min with a person, Hermes finishes | about $1.40 | $3.78 |
| Same, but you are bridged in when the person answers (5 min) | about $1.09 | about $3.56 |
| 2-minute call, a person answers at once | about $0.21 | $0.17 |
Rates behind these (US): outbound $0.014/min, inbound $0.0085/min, ConversationRelay $0.07/min, Media Streams
$0.0044/min, conference $0.0018 per participant-minute (Twilio); Grok Voice $0.08/min and streaming speech-to-text
$0.20/hour (xAI). A US local number was $1.15/month (evidence/twilio/01-preflight.txt).
Measured so far: across the three live calls (80 s, 43 s and 92 s) the Twilio balance went from $13.87156 before
call 1 to $13.78356 after call 3, $0.088 as read from the balance. That figure also covers any other Twilio usage
in that window (for example the filtered inbound text); Twilio had not yet published per-call prices in the
evidence (evidence/trial/*/trial.json). The one audio-free xAI connection was metered at about
$0.0015 (tests/l6/xai-ledger-round6.json). Twilio's published prices for the two Delta runs (toll-free, outbound):
$0.028 for run 1 (68 s) and $0.014 for run 2 (57 s) (provider.price in their trial.json). The live Grok call's
xAI time was estimated at $0.096 by the harness ledger, not by an xAI invoice (grok_evidence in
evidence/trial/call_374a992b53c044f9/trial.json).
Limits and known issues
-
A PIN spoken in slow pieces can partly reach Hermes. The PIN is meant to be keyed; if it is said aloud, the
plugin scrubs it, but a piece said more than 1.5 seconds before the rest has already gone to Hermes when the rest
arrives. Its transcript row is then redacted, provided the piece is still among the last few caller sentences
(within about 10 seconds); text already sent to Hermes cannot be recalled. If you said a real PIN aloud, change it. -
The live bridge (
owner_live_bridge) is untested on Twilio. Both calls join the conference a moment after
the line says "Connecting you now". If moving your call fails, both calls end and nobody is rung back. Twilio may
send conference events for both calls to the first call's address only; the line sends both to the business
call's address and also ends each call when the other ends. -
Task rules are enforced at the call tools. A task's guidance such as "decline any callback offer", "do not pay"
or "do not change the flight" stops Hermes from reporting such a result as done. On such a task the line sends a
menu very little:-
If the menu says anything about a callback, a payment or charge, a change, money (dollars, cost, account,
balance, an amount), or a word like buy, bill, deposit, order, transfer or reserve, nothing at all is sent for
it, not even "agent"; Hermes waits for the next menu. -
Text before the first option must be known greetings, one sentence at a time:
-
"Welcome to " or "Thank you for calling ", where the name is exactly one of the task's
intro_names(for example "Delta Air Lines"; none by default). "Thank you for calling." alone is fine. -
"Your call is important to us", "Please listen carefully, our menu options have changed", "Calls may be
monitored or recorded". -
"Para español, oprima N", which is never pressed.
-
Short hold lines: "Please hold", "Please stay on the line", "All agents are busy", "Your call will be
answered in the order received", "The estimated wait time is N minutes".
Any other text before or after the options also silences the menu, and so does any number that is not the key
being announced (35, 35USD, $35, 1.50) or a currency or surcharge word. Callback wording includes "ring you
back", "we will call you" and "keep your place". -
-
Automated assistants (a business answering with a conversational bot, such as "I'm not a live person.
I'm Delta's automated assistant."): until a person is detected, Hermes says only fixed phrases the line picks:
"Representative.", "Agent.", "Speak to an agent.", " question." (the task'sivr_category:
baggage, reservations or general information), or "No, a general question." when asked whether the call is
about an existing trip.- Any request for a name, number, code, date, card or identity gets "Agent." Callback, payment and change offers
get nothing. - At most one phrase every 8 seconds and 12 per call. No "Am I speaking with a live person?" to a system that
said it is automated. - Such a task is not given up as failed while the call is up: only the business, the time cap or you end it.
- Hermes never speaks over the business: after the opening disclosure, every line it says waits until they have
finished and 1.2 seconds of quiet have passed (quiet_before_speak_s). If they keep talking, after 8
seconds the line goes at the next short pause, and at 10 seconds at the latest. - Your phone rings only after Hermes asked "Am I speaking with a live person?" and heard a plain yes ("Yes",
"Yes, speaking", "I'm a real person", "Yes, this is Dana" …), never on a guess. - If you don't take the call, the fallback question's reply is reported word for word as "unverified" (nobody
checks whether it answers the question), or "unanswered" if they said nothing; the line just says "Thank you
for your time. Goodbye."
- Any request for a name, number, code, date, card or identity gets "Agent." Callback, payment and change offers
-
Every sentence of the menu must be one of the greetings, hold lines or allowed options above, for keys and
speech alike. One unknown sentence, even an option Hermes would not choose ("To repeat, press 9", "For seat
maps, press 2"), means nothing at all is sent for that menu. "Please select an option." on its own gets nothing.
This silences many real menus on purpose; their sentences can be added to the allowlist after review. -
Otherwise a key or reply must be announced as "for reservations / baggage / general information / an agent",
or "to speak to an agent or representative", with nothing else in that option and no text after it. -
One key per menu, and never "yes".
-
A task with a fallback question can only say "agent" or "representative", or press one such key.
A menu reply Hermes was still working on when a person answered, or one that timed out, is dropped. The rules are
fixed patterns, not understanding; write the guidance plainly. -
-
US numbers only. Geographic and toll-free numbers can be called; non-US, premium (900, 976) and US territory
numbers cannot (plugin/line/policy/destinations.py). -
Nine live calls so far: five with the owner's own phones (three outbound, two inbound) and four to Delta's
reservations line. Runs 1 and 2 ended at its automated assistant; runs 3 and 4 reached the real hold queue (14 and
44 minutes) and no person came on. A human agent at a business, a bridge, a real keypad menu, a business calling
back, a code into a live call, owner line mode and barge-in on the Grok path have not happened on a real call. See
the status table. -
Slow turns. On call 2 one turn took 7.3 s to its first text; spoken fillers covered the gap. Turns that end a
call were 5.7 to 7.3 s on call 3; L1 round 12 cut that offline (docs/lanes/L1.mdsection 21). -
Person detection is experimental and misses some real people (status table); a bare "hello" is not enough. Owner mode can fall back to
agent mode if you do not answer (allow_agent_fallback). -
A question when you don't pick up (owner mode, tested offline only). Two extra fields on
hermes_line_request_call, shown in the approval message, change what happens when you don't answer:owner_ring_timeout_s(10 to 60): how long you have to press 1 once you are rung.fallback_question: one general question.
If you don't press 1 in time, the line ends your ring and asks the person that question in a fixed sentence. It
waits for the answer, thanks them and hangs up. The summary is the question and their own words.- Once a person answers, Hermes does not speak on such a call; every line is fixed text. It cannot make up a
booking, a confirmation number or a name. - Callbacks, payments and changes are refused, menu keys are single keys, and a spoken menu reply can only use
words like "agent" or "representative". - The question itself may not contain digits, an identity, a booking or account, or a callback, payment or change.
-
Key tones on the Grok path are played as audio inside the call stream. Whether real menus hear them is
untested; a fallback through Twilio exists but is also untested (docs/lanes/L5.mdsection 5). -
Without the Twilio Auth Token, anyone who learns the secret webhook path can send forged events about a real
call.docs/lanes/L1.mdsection 9 lists exactly what that allows. Use the Auth Token where you can. -
The gateway must stay up. If the Hermes gateway restarts during a call, that call loses its in-call tools
(they fail closed) and the call continues until its time limit or a hang-up. -
The call profile's reasoning setting applies to the call session; a slow setting makes Hermes slower on the phone
(docs/lanes/L1.mdsection 8.2). -
Android 17 may delay or hide fresh verification codes from apps other than the default SMS app. Not measured
yet: the probe found no phone attached (evidence/sms/otp-delay-20261105T184714Z.json). -
Twilio numbers do not receive verification codes (error 30038). Use your carrier phone for codes.
Screenshots
Offline harness renders of the shipped pane code with sample data (tools/pane-preview/README.md says what is real
and what is faked). Not screenshots of a live call.
| On hold | A person picked up | You are connected | Ended, with summary |
|---|---|---|---|
|
|
|
|
|
Dark theme: human_detected-dark.png.
The same pane inside a real Hermes Desktop (isolated sandbox install, same sample data, not a live call):
on hold,
a person picked up,
you are connected,
ended.
Repository layout
plugin/: the plugin (plugin.yaml,__init__.py,line/engine,desktop/plugin.jspane,
dashboard/REST backend,skills/hermes-line/).tests/: offline test suites per part (tests/l1totests/l6).docs/: design records (docs/adr/), lane reports (docs/lanes/), release texts (docs/release/).evidence/: live evidence (Twilio, the live calls, SMS probes, security reviews) with secrets and codes
masked.
README from the reviewed commit a2ff270; it updates when the author re-pins.

uzay-eth