Tools
Agent Security Guard
Deterministic security guard for Hermes Agent (catalog plugin) and OpenClaw: what an agent reads can't decide what it does. Untrusted content loses command authority; reading and summarizing stay free. Stdlib-only.
Install
pip install -e
Configuration Example
tool_tiers:
codebase_search: read_only
cronjob_manage: config_change
my_deploy_tool: execution
README
<p align="center">
<img src="assets/logo.png" alt="agent-security-guard logo" width="180">
</p>
<h1 align="center">agent-security-guard</h1>
<p align="center"><strong>A deterministic transition policy engine for Hermes / OpenClaw agents.</strong><br>Keeps reading, browsing, and summarizing free — strips command-authority from untrusted content.</p>
<p align="center">
<a href="https://github.com/xMannixx/Agent-Security-Guard/actions/workflows/ci.yml"><img src="https://github.com/xMannixx/Agent-Security-Guard/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/python-3.8%2B-blue.svg" alt="Python 3.8+"></a>
<img src="https://img.shields.io/badge/deps-stdlib%20only-success.svg" alt="Dependencies: stdlib only">
<img src="https://img.shields.io/badge/tests-1274%20passing-success.svg" alt="Tests: 1274 passing">
</p>
<p align="center">
<a href="docs/demo/agent-security-guard-demo.mp4"><img src="assets/demo.gif" alt="Demo: a web page tells Hermes to run a command; Agent Security Guard asks first, the command is denied, the user's own request runs free" width="900"></a><br>
<sub>A real Hermes session, 50 s. <a href="docs/demo/agent-security-guard-demo.mp4">Video (MP4)</a> · <a href="https://x.com/Perry2096581714/status/2119758994139185266">on X</a></sub>
</p>
The companion to [`agent-memory`](../Agent%20memory%20skill). The memory skill
protects long-term truth (what may be remembered). This guard protects the
dangerous moment **before** an action: context intake, tool call, memory write,
external action, and chain drift (what untrusted content is allowed to *become*).
---
## The core principle
> A source's trust does not grant it authority over an action.
The guard is **not** a brake on autonomy. In its default `autonomous-safe` mode,
reading, browsing, GET/search, and summarizing run freely. Only risky
transitions are gated, and gated by **hard deterministic rules** — the risk
score is for logging and prioritization, never the sole judge.
It is explicitly **not** an "AI detects bad prompts" toy. It is a policy engine
for transitions.
## Two independent dimensions
Trust and sensitivity are separate axes and are never collapsed:
- **OriginTrust** — *may this source give instructions?*
`trusted_user` > `local_project` / `trusted_tool_output` > `unspecified` >
`tool_output` > `external_web` > `external_document` > `unknown`
- **DataSensitivity** — *how dangerous if it leaks?*
`public` < `internal` < `sensitive` < `secret`
A `.env` file is high-trust origin but secret-sensitivity. Tool output inherits
its payload origin: a web-fetch tool produces `external_web`, not trusted tool
knowledge.
`unspecified` vs `unknown` matters: `unspecified` means the host stated no
provenance (its own internal tool call) and is **not** treated as untrusted;
`unknown` means provenance was examined and could not be established, which is.
## Operating modes (and the off switch)
`mode` in [guard.yaml](guard.yaml) is enforced:
| Mode | Behaviour |
|---|---|
| `monitor` | Never blocks. Reports what a blocking mode *would* have done in `advisory_decision`. Use when introducing the guard into a live host. |
| `autonomous-safe` (default) | Blocks only the narrow, unambiguous danger set. Unrecognized tool kinds are allowed and audited. |
| `strict` | Also stops and asks on unrecognized tool kinds. |
To stop all blocking without editing any file, set this and restart the host:
```bash
export AGENT_SECURITY_GUARD_MODE=monitor
```
The variable is read once, when the guard starts. Changing it inside a running
process has no effect, so code the agent runs cannot use it to switch the guard
off.
If the guard itself cannot evaluate a call (broken install, a bug in the
engine) it degrades instead of denying every call: reads and unrecognized host
tools keep working, flagged `degraded`, while state-changing kinds and kinds
dangerous by name are blocked. Set `on_error: deny_all` to block everything. An
unwritable audit file or a `guard.yaml` that cannot be parsed does not degrade
the guard at all: it keeps evaluating (without audit, or on the built-in
defaults with `config_error` in its decisions).
## Enforcement Mode vs Advisory Mode
- **Enforcement Mode** — the guard sits in the tool-call path and can stop
actions: `pre_tool_call -> check_action -> allow / deny / transform`.
- **Advisory Mode** — the guard only emits recommendations, e.g.
`advise_memory_write -> MemoryAdvice`, without touching the memory skill.
## Hard-rule highlights (Sprint 1, implemented)
| Transition | Decision | reason_code |
|---|---|---|
| read-only / GET / search | `allow` | `ALLOW_READ_ONLY` |
| local read of secret-class content | `require_confirmation` | `SENSITIVE_PATH_READ` |
| untrusted web/doc -> shell | `deny` | `UNTRUSTED_TO_SHELL` |
| after a web page or search result was read in the chain: shell, file write, install, config change, external write, a host memory tool | `require_confirmation` | `UNTRUSTED_CONTENT_IN_CONTEXT` |
| shell from trusted user | `require_confirmation` | `SHELL_FROM_USER_REQUIRES_CONFIRMATION` |
| web-suggested command relayed by a bare "yes" | `deny` | `CONFIRMATION_ORIGIN_UNTRUSTED` |
| install from untrusted | `deny` | `INSTALL_FROM_UNTRUSTED` |
| external write (default) | `require_confirmation` | `EXTERNAL_WRITE_REQUIRES_CONFIRMATION` |
| external write to loopback / allowlisted domain, trusted origin | `allow` | `ALLOW_DEFAULT` |
| external write of secret-class content | `deny` | `SECRET_EXTERNAL_SEND` |
| any request to a remote host with a secret in its URL, query or body (GET included), also through the host's own web, search and browser tools | `deny` | `SECRET_EXTERNAL_SEND` |
| after a secret read: GET-like request with a query string, URL credentials or a body, also through the host's own web tools | `require_confirmation` | `SECRET_THEN_EXFIL` |
| untrusted web/doc -> file write (`write_file`, `patch`, ...) | `deny` | `UNTRUSTED_TO_LOCAL_WRITE` |
| file write from a trusted origin | `allow_with_warning` | `LOCAL_WRITE_AUDITED` |
| file tool writing `SKILL.md` / `guard.yaml`, the audit trail, or the guard's own code | as self-modification | `SELF_MODIFICATION_...` |
| host tool recognized by name, trusted origin, nothing pointing at danger | `allow_with_warning` | `HOST_TOOL_AUDITED` |
| unrecognized tool kind (non-strict) | `allow_with_warning` | `UNKNOWN_ACTION_AUDITED` |
| unrecognized tool kind proposed by untrusted content | `deny` | `UNTRUSTED_TO_UNKNOWN_ACTION` |
| untrusted -> `authorization`/`procedural` memory, also under another name (`auth`, `permissions`, `rules`, `system`, ...) | `deny` | `UNTRUSTED_TO_AUTH_MEMORY` / `..._PROCEDURAL_MEMORY` |
| untrusted -> memory write that names no lane, or one the guard cannot read | `deny` | `UNTRUSTED_TO_UNKNOWN_MEMORY_LANE` |
| untrusted -> `identity` / `preference` memory | `require_confirmation` | `UNTRUSTED_TO_IDENTITY_MEMORY` / `..._PREFERENCE_MEMORY` |
| untrusted -> `evidence` memory | `allow_with_warning` | `UNTRUSTED_TO_EVIDENCE_MEMORY` |
| `authorization`/`procedural` memory on a trusted origin, no source stated | `require_confirmation` | `PRIVILEGED_MEMORY_REQUIRES_CONFIRMATION` |
| `authorization`/`procedural` memory from `observation`, trusted origin | `allow`, audited | `PRIVILEGED_MEMORY_AUDITED` |
| memory write without a readable lane, trusted origin | `allow_with_warning` | `UNKNOWN_MEMORY_LANE_AUDITED` |
| after a web page or search result was read in the chain: memory write without a readable lane | `require_confirmation` | `UNTRUSTED_CONTENT_IN_CONTEXT` |
## Your host's tool names
The rules above are written for the guard's own kinds (`shell`, `http_post`,
`skill_patch`, ...), but a host forwards its tools under its own names: Hermes
calls its shell `terminal`, OpenClaw's file writer is `write`. The guard
recognizes the common ones (`bash`, `terminal`, `execute_code`, `write_file`,
`edit`, `patch`, `apply_patch`, `send_email`, `skill_manage`, `memory`, ..., also behind
a namespace prefix such as `mcp__files__write_file`) and applies the same hard
rules to them: denied from untrusted content, covered by a no-write scope, part
of the exfiltration chain, and held to the self-modification bar when they
touch `SKILL.md` or `guard.yaml`.
Recognition does not make the guard ask more. On a trusted or unspecified
origin, with nothing pointing at danger, a recognized host tool is allowed and
audited as before (`tiers.recognized_host_tool`); only a kind the host names
explicitly, or a tool you declare, gets the confirmation gate of its tier.
For names the guard does not know, or reads wrongly, declare the tier in
`guard.yaml`. A declaration wins over the built-in tables:
```yaml
tool_tiers:
codebase_search: read_only
cronjob_manage: config_change
my_deploy_tool: execution
```
An undeclared, unrecognized tool is allowed and audited, except when the host
reports that untrusted content proposed the call
(`user_intent_origin=untrusted_suggestion`): then it is denied.
## Memory writes
A memory write (`memory_write`, `remember`) is judged by the lane it names:
`evidence`, `preference`, `identity`, `authorization`, `procedural`. The last
two say what the agent may do and how it behaves. They are written from direct
observation only (`memory_source: observation`): from anything untrusted they
are denied, and without a stated source the user is asked. Other spellings of
them (`auth`, `permissions`, `rules`, `system`, ...) are read as what they are.
A write that names no lane, or a lane the guard does not know, is not taken for
evidence. On a trusted origin it is allowed and audited; from an untrusted
source it is denied, and once a web page was read in the chain it is asked
about. Give your memory's own lane names a meaning in `guard.yaml`:
```y
... (truncated)
tools
Comments
Sign in to leave a comment