You ask Hermes to clean up a project directory. It lists the files, notices that node_modules is huge, and proposes a command that looks reasonable: rm -rf node_modules. The real question is not whether the agent can write that shell line. It is how many gates the line must cross before it can become a real process launch.
From a chat window, it is easy to collapse “the model generated a tool call” and “the tool ran” into one event. Hermes keeps them separate. The model can propose a request only from the tool schemas visible in the current turn. The dispatcher then decides whether the tool exists, whether it may run concurrently, and which environment should host it. A terminal command adds dangerous-pattern detection, human approval, and hard stops. Authorization is not one boolean; it is a context-bearing path.
Gate one: what the model can see
When a tool call comes back from the model, Hermes validates its name and arguments. An unknown name is not guessed and executed. It becomes a tool error for the model; after three invalid attempts the turn exits early. Arguments must be a JSON object. Truncated or unparseable arguments are never treated as a half-finished shell command.
An earlier gate happens before the request reaches the model. Each registry ToolEntry may carry a toolset and a check_fn. While OpenAI-format schemas are built, a false check_fn removes the tool from the list; an exception is also treated as unavailable. enabled_toolsets and disabled_toolsets narrow the candidate set, while dynamic MCP tools add discovery and scope checks. “The model cannot see this tool” is itself a permission boundary.
That filter is not the end of authorization. A tool may be visible and still be refused at execution time because of the current environment, path, or approval policy. Treating schema exposure as authorization mistakes prompt-level visibility for runtime capability.

Gate two: decide where it runs
Hermes divides a batch into calls that may run in parallel and calls that must remain sequential. Read-only queries and MCP tools that explicitly opt into parallel calls can share a batch. File writes, overlapping paths, and interactive tools create barriers. Path-aware calls perform overlap checks: two readers may share a subtree, while a writer conflicts with any overlapping reservation. The speed optimization therefore does not silently change the state order represented by tool results.
Before a terminal actually starts, it also binds the turn's environment policy. terminal_scope places a profile's backend, cwd, sandbox, and lifetime settings in a context-local scope. If resolution fails, Hermes installs a refusal scope and later execution raises TerminalPolicyUnavailable instead of falling back to the launch process's ambient environment. This fail-closed behavior matters: in a multiplexed gateway, falling back to ambient variables could carry profile A's authority into profile B's turn.
Credentials follow the same rule. In multiplex mode, secret_scope.get_secret() raises UnscopedSecretError when no profile scope is bound instead of quietly reading os.environ. A bound scope exposes only that profile's mapping; a missing key returns the caller's default and never fills itself from another profile. The authorization boundary therefore reaches into process environment state, not only terminal text.
Gate three: who gets to approve a dangerous command
For rm -rf node_modules, the terminal scans for dangerous patterns before spawning a child process. A match sends the request through the approval module, which chooses a route based on the session, platform, and configuration. CLI mode can show an interactive prompt. Gateway mode can send the request through a callback to the messaging platform. Smart mode can let a secondary model assess a low-risk exception. Unattended webhook, API, and cron contexts deny by default because no reliable human is present to answer /approve.
Approval has scope. Approve once affects one call. A session approval lives only in the current session. “Always allow” writes to the active profile's command_allowlist. Approval queues are keyed by session; a reconnecting client can restore only its own oldest pending request. Clearing a session cancels pending approvals and shuts down related kernels, so a finished conversation does not leave reusable execution resources behind.
This is why a model saying “I approve” is not authorization. Model output is an input to the approval flow; the decision comes from a control plane bound to the platform and session. /yolo is not an ordinary allowlist either. It bypasses bypassable dangerous-command prompts, while the hardline blocklist remains in front of it. Fork bombs, device formatting, and other catastrophic operations do not gain permission through YOLO.

Gate four: classify the result after it returns
A returned string does not mean success. Hermes projects the result back into the conversation and distinguishes a guardrail refusal, a real failure, and a file mutation that actually landed. tool_may_have_side_effect() treats unknown, plugin, and MCP tools as effect-capable by default, so an interrupted result cannot be discarded casually. A write_file result counts as landed only when it contains bytes_written; a patch counts only with success: true.
That classification changes the next turn. A guardrail refusal for a redundant read may contain the word error, yet it is not counted as a failed attempt. A non-zero terminal exit does enter failure recovery and loop guardrails. Repeated calls with no progress can move from warning to block or halt. The agent does not blindly retry every red string; it distinguishes refusal, failure, and a successful call that produced no useful progress.
A design lesson you can carry elsewhere
When connecting an internal MCP to your own agent, answer four questions separately: when does a tool enter the schema; which session and profile issued the call; which environment and secret scope host it; and what evidence proves that its side effect happened. Compressing those questions into one is_allowed() function is shorter at first, but it destroys audit information and cannot explain why the same command may prompt in CLI while being denied by a webhook.
Hermes offers another useful detail: each gate returns structured information that the model can work with. Unknown tools receive a clear error. Approval denial states that nothing ran. Policy resolution failure installs a refusal scope. A file write is successful only when verifiable fields say so. The agent can revise its plan, but it cannot turn a refusal into a success story.
I ran 22 checks against pure functions and small substitutes from the pinned commit. They cover toolset visibility, dangerous-command detection, session approval isolation, YOLO versus hardline behavior, fail-closed secret scopes, result classification, and path-overlap barriers. The checks did not start a real shell, messaging gateway, secondary model, or MCP server. They validate decision boundaries, not end-to-end deployment performance.
When Hermes says that an action can run, the reliable interpretation is precise: the model produced a structurally valid request, it passed the current tool and environment checks, any required approval came from the right session, and the result returned with verifiable state. Remove any one layer and an agent may still sound capable while hiding an authorization problem inside natural language.
Source entry points: tools-runtime, tool-call validation, terminal scope, secret scope, and approval implementation.

