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
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Changelog

## 0.2.0 — 2026-08-23

### Harness reporter (pre-execution enforcement)

- New `src/reporter.mjs`: NDJSON unix-socket server at
`~/.local/state/herdr-guard/reporter.sock` (override with
`HERDR_GUARD_REPORTER_SOCKET`). Agent harnesses report tool calls before
execution and receive an advisory verdict from the same policy:
interrupt→`deny`, alert→`warn`, audit/none→`allow`. Stale socket files are
reclaimed; a live socket from another guard is respected.
- New `hooks/claude-code-pretooluse.mjs`: zero-dependency Claude Code
`PreToolUse` hook mapping `deny` to a blocked tool call and `warn` to a
permission prompt. Strictly fail-open — a stopped guard never breaks the
harness.
- Reported commands bypass `prompt_only` gating (no prompt glyphs in raw
commands), honor project overrides by reported `cwd`, and are audited with
`source: "harness:<agent>"`. Pause allows but still audits. The dashboard
shows a harness-reports counter.

### Default policy hardening (26 → 52 rules)

- New interrupt-tier rules: device wipes (`wipefs`/`blkdiscard`/`shred` on
devices), shell redirects onto block devices, recursive `chmod`/`chown` on
rootish paths, `find / -delete`, fork bombs, `crontab -r`.
- New alert-tier rules: AWS/GCP/Azure resource deletion, PaaS app
destruction, DB `DROP`/`TRUNCATE` (prompt-only), `kubectl delete
namespace` / `helm uninstall`, `docker volume` removal, the package
publish family (`cargo`/`twine`/`gem`/`yarn`/`pnpm`), SSH-key and
credential-store reads, `curl` uploads of secret material, firewall
disabling, guard tampering (plugin disable, killing Herdr, deleting
rules/audit files), history clearing, `setsid`/`at now` detachment,
hex-decode-to-shell, `gh repo delete`, `git push --delete`/`--mirror`.
- False-positive fixes: `git push --force-with-lease` no longer trips the
force-push alert (it has its own audit-tier rule); `id_rsa.pub` reads and
`curl` posts to URLs merely containing "credentials" stay silent.
- New `tests/rules-default.test.mjs`: every shipped rule carries canonical
hits plus near-miss false-positive guards, with completeness enforced.

### Review hardening (post-review fixes, same release)

- Reporter lifecycle: socket claiming is now gated by an atomic pid lock
(concurrent starters cannot orphan each other), `close()` only removes a
socket/lock the instance owns, and a pre-existing parent directory of a
user-overridden socket path is never chmodded.
- Rule fixes: `crontab -u <user> -r` now interrupts; mixed-case SQL
`Drop Table` now alerts; `wipefs` without erase flags, `grep setsid`,
`gcloud ... list | grep delete`, and `curl -d @file` posts to URLs merely
containing "credentials" no longer false-positive.
- Guard and hook now agree that an empty `HERDR_GUARD_REPORTER_SOCKET`
means unset.

## 0.1.1

- Transport hotfix: Herdr socket lifecycle repair, watcher lifecycle and
audit gap fixes, per-session watchdog recovery isolation, malformed
generation report deduplication, minimal CI validation workflow.

## 0.1.0

- Initial release: pane watcher with subscribe-first/reconcile-after
bootstrap, content-based replay suppression, prompt-gated interrupt rules,
severity-partitioned audit log with redaction and sanitization, project
overrides capped at alert, pause/resume with TTL, self-healing watchdog,
ANSI dashboard.
67 changes: 59 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Cross-agent command policy for [Herdr](https://herdr.dev): watch every pane,
audit risky commands, notify you, and best-effort interrupt dangerous shell
input.

Current runtime and manifest release: **0.1.1**.
Current runtime and manifest release: **0.2.0**.

**Docs:** the [StructuPath Herdr Plugins wiki](https://github.com/StructuPath/herdr-browser/wiki)
is the practical guide to this plugin and its three siblings (Browser, Swarm,
Expand All @@ -30,7 +30,49 @@ is not proof that the process received Ctrl+C or that execution changed.

This is a text policy layer, not intent analysis. Shell obfuscation, detached
nested multiplexers, popup panes, and a stopped/disabled guard are documented
limitations. Use native agent hooks for authoritative tool-call enforcement.
limitations. For authoritative tool-call enforcement inside agent harnesses,
use the bundled harness reporter (below) or native agent hooks.

## Harness reporter (pre-execution enforcement)

Pane-watching can only *request* an interrupt after text renders. The
reporter path inverts that: an agent harness reports each tool call to the
guard **before execution** over a local unix socket
(`~/.local/state/herdr-guard/reporter.sock`, dir `0700`) and receives a
verdict from the same policy — `deny` (interrupt-tier), `warn` (alert-tier),
or `allow`. Reported commands are matched raw (prompt-only gating does not
apply) and audited with `source: "harness:<agent>"`; project overrides apply
by the reported `cwd`.

A ready-made Claude Code `PreToolUse` hook ships in
`hooks/claude-code-pretooluse.mjs` — it maps `deny` to a blocked tool call
and `warn` to a permission prompt. Wire it in `settings.json`:

```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node /path/to/herdr-guard/hooks/claude-code-pretooluse.mjs"
}
]
}
]
}
}
```

The hook is strictly fail-open: if the guard is not running, times out, or
answers garbage, the tool call proceeds and nothing breaks. The guard cannot
observe whether a harness honored a verdict, so audit entries still record
`prevention: "unknown"`. Other harnesses can implement the same one-line
NDJSON protocol: send
`{"v":1,"kind":"tool_call","agent":"pi","tool":"shell","command":"...","cwd":"..."}`
and read back `{"ok":true,"verdict":"deny","rule_id":"...","reason":"..."}`.

## Install

Expand Down Expand Up @@ -72,10 +114,18 @@ rules unless the user explicitly enables `allow_project_override` in the
global configuration. Configuration writes are atomic and malformed updates
keep the last known-good policy.

The shipped policy covers destructive filesystem/Git/infrastructure commands,
secret-file reads, publishing, data exfiltration, and evasion indicators such
as `stty -echo`, detached tmux/screen, `disown`, base64-to-shell, and eval
subshells. Review the defaults before enabling interrupt rules in production.
The shipped policy covers destructive filesystem/Git/infrastructure commands
(including cloud-resource deletion on AWS/GCP/Azure, PaaS app destruction,
Kubernetes/Helm teardown, and database `DROP`/`TRUNCATE` statements),
secret-file and credential reads, package publishing, data exfiltration
(`scp`/`rsync` of key directories, `curl` uploads of secret material), guard
tampering (`herdr plugin disable`, killing Herdr, deleting rules or audit
files), and evasion indicators such as `stty -echo`, detached tmux/screen,
`disown`/`setsid`, history clearing, and base64/hex-to-shell decoding. Every
rule ships with hit and near-miss tests (`tests/rules-default.test.mjs`);
`git push --force-with-lease`, `id_rsa.pub` reads, and similar benign
neighbors are explicitly kept silent. Review the defaults before enabling
interrupt rules in production.

## Security and trust

Expand All @@ -94,7 +144,7 @@ fresh interrupt, and interrupt matches are intentionally never deduplicated.

## Development

Requirements: Herdr 0.7.5+, Node.js 20+, and the platform lock utility (`lockf` on macOS or `flock` on Linux).
Requirements: Herdr 0.7.5+, Node.js 20.10+ (JSON import attributes), and the platform lock utility (`lockf` on macOS or `flock` on Linux).

```sh
npm test
Expand All @@ -114,7 +164,8 @@ vhs assets/demo.tape

## Future work

- Harness reporters for Pi/Claude Code tool calls.
- Additional harness reporters (Pi extension, Codex) speaking the shipped
reporter protocol.
- Shell pre-exec approval flow.
- Popup visibility in Herdr's event/API surface.
- Per-plugin socket ACLs or read-only tokens.
Expand Down
47 changes: 30 additions & 17 deletions docs/SHOW_HN.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,20 @@ herdr-guard is a Herdr plugin that applies one text policy across those panes.
Rules can audit a match, send an alert, or attempt to interrupt an interactive
shell by requesting Ctrl+C in the pane that produced the event. Guard records
whether Herdr accepted that request, but prevention remains unknown. The
default policy covers destructive filesystem and infrastructure commands,
force pushes, secret-file reads, publishing, exfiltration indicators, and
common attempts to hide execution.
default policy is 54 rules covering destructive filesystem, git, cloud, and
database commands, secret and credential reads, publishing, exfiltration
indicators, guard tampering, and common attempts to hide execution — and every
rule ships with hit and near-miss tests, because false positives train people
to pause the guard.

The same policy now also enforces before execution. The guard listens on a
local unix socket, and a bundled Claude Code PreToolUse hook reports each tool
call and honors the verdict: interrupt-tier rules deny the call, alert-tier
rules turn into a permission prompt, and everything is audited in one place.
The hook is strictly fail-open — a stopped guard never breaks the harness —
and the protocol is one NDJSON line, so other harnesses can wire in the same
way. Pane-watching stays the cross-agent backstop; the hook is where
prevention actually exists.

The implementation is plain ESM Node.js 20 with no runtime dependencies. It
connects to Herdr's NDJSON socket, takes a pane snapshot, subscribes to
Expand All @@ -39,26 +50,28 @@ are private, rotated, partitioned by severity, sanitized, and redacted before
writing. Socket disconnects are visible and trigger reconnect plus a complete
re-bootstrap.

This is not a sandbox or an intent detector. Interactive Bash and Zsh input is
the strongest case because canonical terminal echo exposes text before Enter.
Commands executed internally by Pi, Claude Code, or Codex TUIs are usually not
visible unless the TUI renders them. Raw/no-echo shells, popup panes, nested
multiplexers, and semantic obfuscation are additional blind spots. Native agent
hooks remain the authoritative enforcement point for tool calls.
This is not a sandbox or an intent detector. On the pane side, interactive
Bash and Zsh input is the strongest case because canonical terminal echo
exposes text before Enter; commands executed internally by Pi, Claude Code,
or Codex TUIs are usually not visible unless the TUI renders them — which is
exactly the gap the reporter hook closes for Claude Code. Raw/no-echo shells,
popup panes, nested multiplexers, and semantic obfuscation remain blind spots,
and a process with the user's privileges can still stop the guard (the hook
fails open by design; tampering attempts are alert rules).

You can try the tagged release without an account or service. It requires
Herdr 0.7.5+, Node.js 20+, and macOS or Linux:
Herdr 0.7.5+, Node.js 20.10+, and macOS or Linux:

```sh
herdr plugin install StructuPath/herdr-guard --ref v0.1.1
herdr plugin install StructuPath/herdr-guard --ref v0.2.0
```

The repository includes the policy, manifest, an honest coverage matrix, a
reproducible demo, and a fake-socket/runtime regression suite. I would
especially value feedback on false-positive tradeoffs, useful default rules,
and whether the next step should be shell pre-exec approval, small reporters
for agent harnesses, or upstream Herdr capabilities such as popup visibility
and socket ACLs.
The repository includes the policy, manifest, an honest coverage matrix, the
Claude Code hook, a reproducible demo, and a fake-socket/runtime regression
suite. I would especially value feedback on false-positive tradeoffs, useful
default rules, and whether the next step should be reporters for more
harnesses (Pi, Codex), shell pre-exec approval, or upstream Herdr
capabilities such as popup visibility and socket ACLs.

## Posting notes

Expand Down
83 changes: 70 additions & 13 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# herdr-guard — Spec (v1, runtime 0.1.1)
# herdr-guard — Spec (v1, runtime 0.2.0)

Cross-agent command policy layer for [Herdr](https://herdr.dev). Watches herdr
panes for dangerous commands, then audits, alerts, or interrupts — from one
Expand All @@ -21,8 +21,9 @@ The guard matches command **text**, not command **intent**. Semantic
obfuscation (`base64 -d | sh`, `r''m`, `$x -rf`, python `shutil.rmtree`)
defeats content matching; obfuscation-*indicator* alert rules make attempts
loud but can't stop them. Harness-level hooks remain the enforcement point
inside TUI agents; v2 path is tiny harness reporters (Pi extension / Claude
Code hook) POSTing tool calls to the guard for unified audit+policy.
inside TUI agents — and the guard now ships that path: harness reporters
(see "Harness reporter ingest" below) submit tool calls pre-execution for a
unified audit + policy verdict the harness can enforce.

## Architecture

Expand All @@ -40,9 +41,50 @@ herdr-guard/
rules-default.json # shipped default policy (seeded into config dir)
audit.mjs # JSONL append, redaction, sanitization, partitioned rotation
render.mjs # dashboard rendering (ANSI, sanitized)
reporter.mjs # harness reporter ingest: NDJSON unix-socket server
hooks/
claude-code-pretooluse.mjs # shipped Claude Code PreToolUse reporter (fail-open)
tests/*.test.mjs # node:test — policy engine + socket client (fake NDJSON server)
```

### Harness reporter ingest

The one place prevention is actually possible: an agent harness reports each
tool call BEFORE execution and can honor the verdict.

- Transport: NDJSON request/response over a unix socket at the well-known
per-user path `$XDG_STATE_HOME/herdr-guard/reporter.sock` (default
`~/.local/state/herdr-guard/reporter.sock`) — deliberately NOT the
per-session herdr state dir, because reporters run inside agent processes
without herdr's plugin environment. Dir `0700` (created only if missing —
a user-overridden path never gets its existing parent chmodded), socket
`0600`. Claiming is race-safe: an atomic (O_EXCL) pid lock file gates the
unlink-and-bind, a dead holder's lock is reclaimed, and `close()` removes
only a socket/lock the instance owns — a losing guard's shutdown can never
delete the surviving guard's live socket. A live socket (another guard) is
left alone and logged. Override with `HERDR_GUARD_REPORTER_SOCKET` (empty
means unset, on both the guard and hook sides).
- Request: `{v:1, kind:"tool_call", agent, tool, command, cwd?, session?}`.
Response: `{ok, verdict: "deny"|"warn"|"allow", enforcement, rule_id,
severity, reason}`. Mapping: interrupt→deny, alert→warn, audit/none→allow.
- Matching runs with `paneType: "harness"` so `prompt_only` never gates a
raw reported command (there are no prompt glyphs to find). Project
overrides merge by the reported `cwd`. Multi-line commands take the
worst-line verdict.
- Every match is audited (`source: "harness:<agent>"`, decision
`advise-deny` / `advise-warn` / `log-only`, `prevention: "unknown"` —
the guard cannot observe whether the harness honored the verdict).
Dedupe/rate-limit/notification-coalescing reuse the pane pipeline with a
synthetic `harness:<agent>` pane key; interrupt-tier is never suppressed.
`pause` yields `allow` + `enforcement: "paused"` while still auditing —
same contract as panes: pause stops actions, never the record.
- Shipped reporter: `hooks/claude-code-pretooluse.mjs`, a zero-dependency
Claude Code PreToolUse hook. deny → `permissionDecision: "deny"`, warn →
`"ask"`, allow → silent. Strictly fail-open (500ms deadline, exit 0 on
any failure): a broken or absent guard must never break the harness.
Residual: killing the guard silences this path; pane-side tamper rules
make that loud.

### The Guard pane (watcher)

One long-running `[[panes]]` entrypoint (`placement = "split"`). Lifecycle:
Expand Down Expand Up @@ -158,17 +200,32 @@ One long-running `[[panes]]` entrypoint (`placement = "split"`). Lifecycle:
validator at load: cap length 512, reject backrefs, reject unparseable
regex (log + notify on rejection).
- **Default rules** (from pi damage-control, pi-library sp-damage-control
- safe-mode, red-team additions):
- safe-mode, red-team additions; every rule has hit/near-miss coverage in
`tests/rules-default.test.mjs`):
- *interrupt*: `rm -rf` rootish paths, `dd of=/dev`, `mkfs`,
`git push --force` / `reset --hard` (alert or interrupt — ship alert),
`terraform destroy`, `kubectl delete` prod-ish contexts
- *alert*: `sudo`, `curl|sh` / `wget|sh`, `cat .env*` / `security
find-generic-password -w`, `npm publish`, `aws s3 rm|sync --delete`,
`docker system prune -a`, exfil (`scp|rsync` of `~/.ssh`, `~/.aws`,
`~/fsw-bid-data`), **evasion indicators**: `stty -echo`, `stty raw`,
`tmux.*(-d|-b)`, `screen -dm`, `disown`, `base64 -d` piped to shell,
`eval $(`, `sh -c "$(`
- *audit*: everything above plus git destructive variants
`wipefs`/`blkdiscard`/`shred` on devices, shell redirects onto block
devices, recursive `chmod`/`chown` on rootish paths, `find / -delete`,
fork bombs, `crontab -r`, `terraform destroy`, `kubectl delete`
prod-ish contexts
- *alert*: `sudo`, `curl|sh` / `wget|sh`, `cat .env*` / SSH-key and
credential-store reads / `security find-generic-password -w`,
`npm publish` and the wider publish family (`cargo publish`,
`twine upload`, `gem push`, `yarn`/`pnpm publish`), `aws s3
rm|rb|sync --delete`, AWS/GCP/Azure resource deletion, PaaS app
destruction, DB `DROP`/`TRUNCATE` (prompt-only), `kubectl delete
namespace` / `helm uninstall`, `docker system prune -a` and volume
removal, `git push --force|--mirror|--delete` (force-with-lease is
audit-only), `gh repo delete`, firewall disabling, exfil (`scp|rsync`
of `~/.ssh`, `~/.aws`, `~/fsw-bid-data`; `curl` uploads of secret
material), **guard tampering** (`herdr plugin disable`, killing
Herdr, deleting guard rules/audit files), **evasion indicators**:
`stty -echo`, `stty raw`, `tmux.*(-d|-b)`, `screen -dm`, `disown`,
`setsid`, `| at now`, history clearing (`history -c`,
`HISTFILE=/dev/null`, `unset HISTFILE`), `base64 -d` / `xxd -r` /
`printf '\x..'` piped to shell, `eval $(`, `sh -c "$(`
- *audit*: git destructive variants (`clean -f`, `checkout -- .`,
`branch -D`, `filter-branch`/`filter-repo`, `stash drop|clear`,
`push --force-with-lease`), generic `rm -rf`
- **Project override**: `<workspace cwd>/.herdr-guard.json`, merged lazily
per pane cwd.
- May **add** rules (`substring` only — repo-controlled regex never
Expand Down
2 changes: 1 addition & 1 deletion herdr-plugin.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
id = "structupath.guard"
name = "Guard"
version = "0.1.1"
version = "0.2.0"
min_herdr_version = "0.7.5"
description = "Cross-agent command policy: audit, alert, and best-effort interrupt"
platforms = ["macos", "linux"]
Expand Down
Loading
Loading