Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

## Unreleased

- AgentGuard command and documented installer results now pass through without post-tool blocking or redaction in Codex, Claude Code, OpenClaw, QClaw, Hermes, and DSH. Claude batch inspection excludes recognized own-command calls while retaining checks for other calls; execution-time approval remains.
- Attribute installer results by their output signature in native Codex and Claude Code hooks, without a URL allowlist.

### Changed
- Clean runtime events stay local across DSH, Codex, and Claude Code.
- Reportable DSH and native-hook events now send bounded, redacted Cloud summaries with useful command previews, action details, web destinations, or file/configuration/model/policy context. Raw responses, output, credentials, and other sensitive details stay local.
- Native `PreToolUse` actions remain the primary Cloud records. Routine successful `PostToolUse` observations stay local; failures, elevated risks, anomalous results, and material new findings are uploaded as redacted events.

### Fixed
- Blocked Claude Code `PostToolBatch` findings now reach Cloud with a redacted summary and batch counts.
- Codex and Claude Code hooks no longer flag masked output from the exact standalone `agentguard status` command as PII or bulk egress; modified commands and unmasked secrets remain inspected.
- Native Codex and Claude Code hooks no longer block standalone `agentguard` CLI results when output contains sensitive data. Codex replaces model-visible tool results with locally redacted feedback, Claude Code rewrites supported post-tool results, and the exact pending Agent JWT activation URL remains clickable. Compound commands and other tools remain protected.
- OpenClaw and QClaw integrations now rewrite correlated AgentGuard CLI text in supported OpenClaw-owned `tool_result_persist` transcripts; the native Hermes `transform_tool_result` hook and DSH `tools/post-execute` accept path provide equivalent non-blocking redaction where supported. An exact saved activation link stays clickable; other hooks and downstream policy decisions retain their existing boundaries.

## [1.2.1] - 2026-09-29

Expand Down
4 changes: 2 additions & 2 deletions docs/claude-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ Hook execution is bounded to 30 seconds. Claude Code may continue the original a

`PreToolUse` dynamically classifies Bash, PowerShell, Read, Write/Edit/MultiEdit, Web, MCP, image/file readers, and unknown tools from `tool_name` and `tool_input`. A block uses Claude Code's native `permissionDecision: "deny"`; `require_approval` uses `permissionDecision: "ask"`. Approval depends on Claude Code having an interactive approval UI. Non-interactive runs may not provide a usable approval path, so approval availability is not claimed as universal.

`PostToolUse` scans `tool_response`. AgentGuard uses `updatedToolOutput` only for verified response shapes: Read, Bash/PowerShell, WebFetch/WebSearch, and MCP content results. Redaction preserves the verified string/object/array shape. Unknown output schemas are not guessed or destructively rewritten; sensitive unknown output is marked `partial`/`would_block`, and `PostToolBatch` remains the continuation gate.
`PostToolUse` scans `tool_response` except for recognized AgentGuard commands and the documented installer, whose output passes through unchanged. AgentGuard uses `updatedToolOutput` only for verified response shapes: Read, Bash/PowerShell, WebFetch/WebSearch, and MCP content results. Redaction preserves the verified string/object/array shape. Unknown output schemas are not guessed or destructively rewritten; sensitive unknown output is marked `partial`/`would_block`, and `PostToolBatch` remains the continuation gate for other commands.

Routine successful `PostToolUse` observations remain in the local audit and are not uploaded. Failures, elevated risks, anomalous results, and other material new findings are uploaded as separate Cloud events containing the safe original-action summary, redacted findings, and bounded status metadata.

`PostToolUseFailure` records only redacted failure type/summary and coverage. It cannot replace or block a failed tool's output. Whether a particular Claude Code version includes failed results in a following `PostToolBatch.tool_calls[].tool_response` is `unverified` until tested on that version; if absent, the failure-output path is `unsupported`.

`PostToolBatch` scans each official `tool_calls[].tool_response` result Claude is about to use, records bounded file-path, byte, and redaction counts, and can stop the agentic loop before the next model call. Material findings are uploaded even when continuation is blocked, using only a bounded tool-and-finding summary, static policy titles, and aggregate counts; raw tool results, sensitive values, and absolute paths remain local. Tool reads and side effects have already occurred. Existing transcript content is not removed, and a resumed session is not guaranteed to suppress retransmission.
`PostToolBatch` scans each official `tool_calls[].tool_response` result Claude is about to use, records bounded file-path, byte, and redaction counts, and can stop the agentic loop before the next model call. This result is serialized model content, commonly a string or text-block array, rather than the structured `PostToolUse` response. For a recognized AgentGuard command or the documented installer, AgentGuard excludes the entire call from its inspection copy without changing the host result; other calls in the batch remain protected. Material findings are uploaded even when continuation is blocked, using only a bounded tool-and-finding summary, static policy titles, and aggregate counts; raw tool results, sensitive values, and absolute paths remain local. Tool reads and side effects have already occurred. Existing transcript content is not removed, and a resumed session is not guaranteed to suppress retransmission.

`ConfigChange` reads the event source and, when `file_path` is present, at most the first 256 KiB of new local file content to detect endpoint changes, key forwarding, and dangerous permissions. Source-only events remain metadata-only partial coverage. A block prevents the current session from applying the change; it does not roll back the file on disk.

Expand Down
49 changes: 22 additions & 27 deletions docs/privacy-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,28 @@ uploaded to Cloud.

## AgentGuard-owned CLI results

A single `agentguard` shell invocation with a supported result shape is locally
redacted when its result contains recognized sensitive text. Unknown or malformed
result shapes are blocked. Simple `command`, `env`, and
shell wrappers are recognized; an explicit executable path must resolve to this
package's CLI. Supported native result hooks redact recognized secrets in
host-specific result or persistence surfaces. The documented
`TYPESAFE_API_KEY=<key>` example remains unchanged. The **exact**
pending Agent JWT activation URL saved in the local AgentGuard configuration
remains unchanged when it appears as an exact value, including a structured
`agentRegisterUrl` field or a link followed by sentence punctuation. Encoded
links and other tokenized URLs are not exempt. Compound commands, unverified
executable paths, and unrelated tools retain their ordinary protection;
`agentguard approve` is not exempt from agent-self-approval checks.
Direct `npx @goplus/agentguard` calls with supported public subcommands receive
the same native output treatment. The exact documented
`curl -fsSL https://agentguard.one/install.sh | bash` command receives it only
when the result contains the complete saved activation link. Other installer
results follow ordinary privacy checks. Running the remote installer still
requires approval before execution.

| Host | Own-command result handling | Limit |
|---|---|---|
| Codex | `PostToolUse` replaces sensitive results with redacted feedback. | Requires the native hook; no raw output replacement for unsupported lifecycle events. |
| Claude Code | `PostToolUse` rewrites supported result shapes; `PostToolBatch` excludes an own-command result only after confirming that its model-visible content is already safe. | `PostToolUseFailure` has no output rewrite channel; a sensitive failure result in the batch stops continuation. |
| OpenClaw / QClaw | `tool_result_persist` synchronously rewrites matched text in OpenClaw-owned session transcripts. | Requires host support and a matching `toolCallId`; cannot change results already displayed or Codex-native records. |
| Hermes | Native `transform_tool_result` rewrites results before model context; `/agentguard` uses the same redactor. | Legacy shell hooks expose only an audit-only post event and cannot rewrite tool output. |
| DSH | `tools/post-execute` can return redacted text or structured values with `kind: accept`. | Results blocked by another policy are not rewritten by this hook. |
Standalone `agentguard` commands and direct `npx @goplus/agentguard` commands
pass through post-tool hooks without blocking or redaction, regardless of their
output content or result shape. Simple `command`, `env`, and shell wrappers are
recognized; an explicit executable path must resolve to this package's CLI.
Commands that explicitly redirect executable lookup or Node module loading do
not receive this exemption.
An HTTPS `curl -fsSL …/install.sh | bash` result receives the same exemption
only when its output has the AgentGuard Cloud installer header near the start
and ordered installation stage markers. The URL host is not used as an allowlist. These
markers can be imitated by another script; this is the accepted limit of
attributing a merged shell result without a trusted process identity. In Claude Code,
`PostToolBatch` excludes the whole recognized call from its inspection copy,
while retaining the host's original result. Other calls in a mixed batch remain
subject to the normal privacy gate. Compound commands, unverified paths, and
unrelated tools retain ordinary protection. The installer and `agentguard
approve` still receive their normal checks before execution.

This intentionally means credentials or personal data printed by AgentGuard
commands can reach the agent unchanged. AgentGuard also does not rewrite those
results in OpenClaw, QClaw, Hermes, or DSH. Claude `MessageDisplay` protects
assistant-authored display text separately; it cannot establish which command
produced a sentence in that text.

## Public web-search results

Expand Down
3 changes: 1 addition & 2 deletions plugins/hermes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ Or copy this directory to `~/.hermes/plugins/agentguard/` manually.
| `post_api_request` | Observes and correlates main-loop responses; never blocks them. |
| `pre_tool_call` | Evaluates the call; returns `{"action":"block","message":...}` to veto a dangerous action. |
| `post_tool_call` | Audit-only; never blocks. |
| `transform_tool_result` | Redacts AgentGuard CLI results before model context; preserves the saved JWT activation link. |
| `on_session_start` | Best-effort background scan of installed skills (opt out: `AGENTGUARD_HERMES_AUTOSCAN=0`). |
| `/agentguard` | Slash command — `status`, `report` (default), or `checkup`. |
| `/agentguard` | Slash command — `status`, `report` (default), or `checkup`; output passes through unchanged. |

Tools evaluated (others pass through untouched): `terminal`, `execute_code`,
`write_file`, `patch`, `skill_manage`, `read_file`, `web_search`, `web_extract`,
Expand Down
19 changes: 1 addition & 18 deletions plugins/hermes/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,19 +258,6 @@ def _observe_llm(
request_id=request_id,
)

def redact_self_output(self, text: str) -> str:
"""Use AgentGuard's shared local redactor for one own-command result."""
bin_path = os.environ.get("AGENTGUARD_BIN") or shutil.which("agentguard")
if not bin_path:
raise RuntimeError("AgentGuard redactor is not installed")
proc = subprocess.run(
[bin_path, "self-output"], input=text, capture_output=True,
text=True, timeout=self.timeout, encoding="utf-8",
)
if proc.returncode != 0:
raise RuntimeError("AgentGuard redactor failed")
return proc.stdout

def run_cli(self, args: list) -> str:
"""Run an ``agentguard`` subcommand and return stdout (for /agentguard)."""
bin_path = os.environ.get("AGENTGUARD_BIN") or shutil.which("agentguard")
Expand All @@ -287,11 +274,7 @@ def run_cli(self, args: list) -> str:
)
except subprocess.SubprocessError as exc:
return "AgentGuard CLI failed: %s" % exc
output = (proc.stdout or proc.stderr or "").strip()
try:
return self.redact_self_output(output)
except Exception:
return "AgentGuard output could not be safely redacted; run the command directly in your terminal."
return proc.stdout or proc.stderr or ""

# -- internals ---------------------------------------------------------

Expand Down
69 changes: 0 additions & 69 deletions plugins/hermes/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from __future__ import annotations

import os
import shlex
import logging
import shutil
import subprocess
Expand All @@ -34,7 +33,6 @@ def register(ctx: Any, bridge: Optional[AgentGuardBridge] = None) -> None:
ctx.register_hook("post_api_request", _make_post_api_request(guard))
ctx.register_hook("pre_tool_call", _make_pre_tool_call(guard))
ctx.register_hook("post_tool_call", _make_post_tool_call(guard))
ctx.register_hook("transform_tool_result", _make_transform_tool_result(guard))
ctx.register_hook("on_session_start", _make_session_start(guard))

register_command = getattr(ctx, "register_command", None)
Expand Down Expand Up @@ -138,73 +136,6 @@ def post_tool_call(tool_name: str, args: Optional[Dict[str, Any]] = None, **kwar
return post_tool_call


def _is_standalone_agentguard_command(command: Any, depth: int = 0) -> bool:
if depth > 2:
return False
if not isinstance(command, str) or any(c in command for c in ";&|<>`\n\r\t") or "$(" in command:
return False
try:
tokens = shlex.split(command)
except ValueError:
return False
if not tokens:
return False
index = 0
while index < len(tokens) and _is_shell_assignment(tokens[index]):
index += 1
if index < len(tokens) and os.path.basename(tokens[index]) == "env":
index += 1
while index < len(tokens) and tokens[index].startswith("-"):
index += 1
while index < len(tokens) and _is_shell_assignment(tokens[index]):
index += 1
while index < len(tokens) and tokens[index] in ("command", "builtin"):
index += 1
if index >= len(tokens):
return False
executable = tokens[index]
if executable == "agentguard":
return True
installed = os.environ.get("AGENTGUARD_BIN") or shutil.which("agentguard")
if (os.path.isabs(executable) and os.path.basename(executable) == "agentguard"
and installed and os.path.realpath(executable) == os.path.realpath(installed)):
return True
if os.path.basename(executable) in ("sh", "bash", "zsh", "dash", "fish"):
for flag_index in range(index + 1, len(tokens) - 1):
flag = tokens[flag_index]
if not flag.startswith("-"):
break
if "c" in flag:
return _is_standalone_agentguard_command(tokens[flag_index + 1], depth + 1)
return False


def _is_shell_assignment(token: str) -> bool:
name, separator, _value = token.partition("=")
return bool(separator) and name.isidentifier()


def _make_transform_tool_result(guard: AgentGuardBridge):
def transform_tool_result(tool_name: str, args: Optional[Dict[str, Any]] = None, **kwargs: Any):
if tool_name not in ("terminal", "execute_code") or not isinstance(args, dict):
return None
command = args.get("command") or args.get("code")
if not _is_standalone_agentguard_command(command):
return None
result = kwargs.get("result")
if not isinstance(result, str):
return None
try:
redacted = guard.redact_self_output(result)
return redacted if redacted != result else None
except Exception:
# This hook runs before Hermes puts the result in model context.
# Never hand raw credentials to the model if the local redactor fails.
return "AgentGuard output could not be safely redacted; run the command directly in your terminal."

return transform_tool_result


def _make_session_start(guard: AgentGuardBridge):
def on_session_start(*_args: Any, **_kwargs: Any):
# Best-effort background scan of installed skills, mirroring the shell
Expand Down
1 change: 0 additions & 1 deletion plugins/hermes/tests/test_allow.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ def test_register_wires_all_hooks_and_command():
"post_api_request",
"pre_tool_call",
"post_tool_call",
"transform_tool_result",
"on_session_start",
}
assert "agentguard" in ctx.commands
Expand Down
Loading
Loading