# Five rules for talking to many agents, with a five-check quick check (free sample of Agent Control Room)

Version 1.0, 2026-10-28. By PaperChaseWebb. For anyone steering Claude Code, Codex, Hermes or Grok Bot sessions by voice or dictation. One Markdown file and one Python 3 script, standard library only. The script was tested on its worked example (5 findings, then 0) and on six spoken lines. It was not run with a microphone, and nothing here was loaded inside a harness for this release.

Voice tools for agents are getting good at hearing you. The risk is what they do with what they hear. If "approve it" from across the room, a misheard name, or a podcast in the background can merge a branch, the voice layer has become the weakest approval in your setup. This sample gives you the rules we hold to and a small script that checks a control room config against five of them, plus a test for whether a spoken line is a yes the room must refuse.

## Install in one line

Save this page as FREE-SAMPLE.md, then:

```
python3 -c "import re;t=open('FREE-SAMPLE.md').read();open('room_mini.py','w').write(re.search(r'~~~python\n(.*?)~~~',t,re.S).group(1))"
python3 room_mini.py said "approve it"
```

## Five rules

1. **Voice routes, reports and stops. It never approves.** "Status", "tell juniper to fix the login test" and "stop all" are fine by voice. "Yes", "approve it", "go ahead", "always allow" and "1" are refused, every time, whatever the agent is asking.
2. **One owner, one typed yes, tied to a hash.** A gated action (deploy, merge, push, delete, pay, publish, send) gets a request file. The owner approves it at the keyboard, and the yes names the sha256 of that file. Change the request and the yes no longer counts. The agent checks before it acts.
3. **Names that cannot be confused.** No two agents should sound alike ("rook" and "brook"), and no agent should be called a command or yes word ("status", "go"). A near miss gets a read-back question, never a guess.
4. **Unclear words are read back, not acted on.** If your speech tool gives a confidence score, anything below about 0.75 is repeated back to you. Every action is read back in a sentence.
5. **Stay on your machine.** The board listens on 127.0.0.1. Audio is not kept. Remote speech-to-text or a wider bind is a decision you record, not a default.

## The five checks

| Id | Check |
|---|---|
| M1 | owner is set and not the placeholder |
| M2 | approval_channels holds only typed and file |
| M3 | no two agent names or aliases sound alike |
| M4 | min_confidence is between 0.6 and 1 |
| M5 | the board binds 127.0.0.1 |

Run `python3 room_mini.py room.config.json`. Exit 0 means clean, 1 means findings, 2 means the file is missing. It writes nothing.

## Worked example

A config as many people first write it (example/mini/before.json):

```json
{
  "owner": "",
  "approval_channels": ["typed", "voice"],
  "min_confidence": 0.2,
  "bind": {"host": "0.0.0.0", "port": 8766},
  "agents": [
    {"name": "atlas", "harness": "claude-code", "folder": "api"},
    {"name": "rook", "harness": "hermes", "folder": "research"},
    {"name": "brook", "harness": "codex", "folder": "web"}
  ]
}
```

```
$ python3 room_mini.py before.json
M1 no owner, so no yes can be checked against anyone
M2 approval_channels includes voice; voice is convenience, not a yes
M3 'rook' and 'brook' sound alike; rename one so a misheard name cannot reach the wrong agent
M4 min_confidence is 0.2; use 0.6 to 1 so unclear words are read back
M5 board binds 0.0.0.0; keep it on 127.0.0.1
5 checks, 5 findings
```

The same config fixed (after.json): an owner, typed and file only, brook renamed to juniper, 0.75, loopback.

```json
{
  "owner": "Sam Owner",
  "approval_channels": ["typed", "file"],
  "min_confidence": 0.75,
  "bind": {"host": "127.0.0.1", "port": 8766},
  "agents": [
    {"name": "atlas", "harness": "claude-code", "folder": "api"},
    {"name": "rook", "harness": "hermes", "folder": "research"},
    {"name": "juniper", "harness": "codex", "folder": "web"}
  ]
}
```

```
$ python3 room_mini.py after.json
5 checks, 0 findings
```

And the yes test on six lines:

```
$ python3 room_mini.py said "approve it"                     -> refuse (exit 1)
$ python3 room_mini.py said "Yeah, go ahead."                -> refuse (exit 1)
$ python3 room_mini.py said "always allow"                   -> refuse (exit 1)
$ python3 room_mini.py said "yes merge it"                   -> refuse (exit 1)
$ python3 room_mini.py said "1"                              -> refuse (exit 1)
$ python3 room_mini.py said "tell juniper to run the tests"  -> ok (exit 0)
```

## One prompt to try

"Read my voice setup for [tool] and tell me every way a spoken word can approve, allow or answer a permission prompt in it today. List each one with where the setting lives. Do not change anything."

## room_mini.py

~~~python
#!/usr/bin/env python3
"""Free mini check for a control room config. Five checks, read only.
Usage: python3 room_mini.py room.config.json   Exit 0 clean, 1 findings, 2 file missing.
       python3 room_mini.py said "approve it"   Exit 1 if the line is a yes the room must refuse."""
import difflib, json, os, re, sys


def norm(s):
    return re.sub(r"[^a-z0-9]", "", s.lower())


def main(path):
    if not os.path.isfile(path):
        print(f"no file at {path}", file=sys.stderr)
        return 2
    cfg = json.load(open(path))
    F = []
    if not str(cfg.get("owner", "")).strip() or cfg.get("owner") == "Your Name":
        F.append("M1 no owner, so no yes can be checked against anyone")
    chans = [str(c).lower() for c in cfg.get("approval_channels", ["typed", "file"])]
    if any(c not in ("typed", "file") for c in chans):
        F.append("M2 approval_channels includes " + ", ".join(c for c in chans if c not in ("typed", "file")) + "; voice is convenience, not a yes")
    labels = [(norm(l), a.get("name")) for a in cfg.get("agents", []) for l in [a.get("name", "")] + a.get("aliases", [])]
    for i, (a, n1) in enumerate(labels):
        for b, n2 in labels[i + 1:]:
            if n1 != n2 and (a == b or difflib.SequenceMatcher(None, a, b).ratio() >= 0.75):
                F.append(f"M3 '{a}' and '{b}' sound alike; rename one so a misheard name cannot reach the wrong agent")
    mc = cfg.get("min_confidence")
    if not isinstance(mc, (int, float)) or not 0.6 <= mc <= 1:
        F.append(f"M4 min_confidence is {mc!r}; use 0.6 to 1 so unclear words are read back")
    host = (cfg.get("bind") or {}).get("host", "127.0.0.1")
    if host not in ("127.0.0.1", "localhost", "::1"):
        F.append(f"M5 board binds {host}; keep it on 127.0.0.1")
    for f in F:
        print(f)
    print(f"5 checks, {len(F)} findings")
    return 1 if F else 0


YES = ["always allow", "approve it", "allow it", "go ahead", "sounds good", "do it", "yes", "yeah", "yep", "yup", "ok",
       "okay", "sure", "approve", "approved", "allow", "go", "confirm", "accept", "proceed", "continue", "y", "1", "2", "3", "one", "two"]
FILLER = {"it", "that", "please", "just", "all", "this", "now", "then"}


def said(text):
    t = re.sub(r"\s+", " ", re.sub(r"[^a-z0-9 ]+", " ", text.lower().replace("'", ""))).strip()
    starts = re.match(r"^(approve|allow|accept|confirm)\b", t) or re.match(r"^(yes|ok|okay|sure) (approve|allow|merge|deploy|do|go|push|release|send)\b", t)
    rest = t
    for y in YES:
        rest = re.sub(r"\b%s\b" % re.escape(y), " ", rest)
    if starts or all(w in FILLER for w in rest.split()):
        print(f"refuse: '{text}' is a yes. Voice never approves; type the yes at your keyboard.")
        return 1
    print(f"ok: '{text}' is not a yes; route it to one named agent and read it back.")
    return 0


if __name__ == "__main__":
    if sys.argv[1:2] == ["said"] and len(sys.argv) > 2:
        sys.exit(said(" ".join(sys.argv[2:])))
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "room.config.json"))
~~~

## What the full pack adds

Agent Control Room ($29) adds the router for a dozen agents (exact names, aliases, sound-alike questions, one agent per command), holds on risky verbs that only a typed `send` releases, a refusal list for every kind of yes, request files with a typed approval tied to the sha256 and a verify command agents run before acting, tmux delivery that refuses to type into a waiting prompt, a status board with waiting on top (terminal, HTML, read-only on 127.0.0.1), a results inbox read aloud with secrets redacted, hooks for Claude Code, Codex and Hermes, a skill file, an AGENTS.md snippet and a Grok Bot rules block, a labeled set of 48 spoken commands, and a fifteen-check read-only checker tested on a seeded fixture. If you want it set up across your team, there is a $5,000 Install sprint and $5,000/mo Manage. Book a call from paperchasewebb.ai.

Credit: the idea owes a lot to Jeremiah Daws's Agent Bridge, shown on X (https://x.com/JeremiahDaws/status/2113619866028589312). Nothing here is copied from it.

PaperChaseWebb 💸
