← Back to Plugins
Tools

Agent Security Guard

xMannixx By xMannixx 👁 31 views ▲ 0 votes

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.

GitHub

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

Loading comments...