diff --git a/README.md b/README.md index 23ddb35..05a5868 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ For most users, start with these: | `components/hermes/hermes-memory-encrypt` | Encrypts `MEMORY.md` and `USER.md` at rest with HMAC verification. | | `components/hermes/hermes-brain-backup` | Creates sanitized backups of agent state while excluding credentials and sessions. | | `components/hermes/hermes-commit-guard` | Injects commit policy and provides an optional commit-msg hook to block AI tell phrases. | +| `components/hermes/hermes-hook-audit` | Writes redacted JSONL records for Hermes shell-hook activity without changing agent behavior. | | `components/hermes/hermes-context-guard` | Adds context-handling rules for safer agent behavior. | | `components/hermes/hermes-self-review` | Injects a final review checklist before delivery. | | `components/hermes/hermes-hardened-skills` | Skill prompts for common software/security roles. | diff --git a/components/hermes/hermes-hook-audit/.gitignore b/components/hermes/hermes-hook-audit/.gitignore new file mode 100644 index 0000000..5c909ff --- /dev/null +++ b/components/hermes/hermes-hook-audit/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +*.log +*.jsonl diff --git a/components/hermes/hermes-hook-audit/CHANGELOG.md b/components/hermes/hermes-hook-audit/CHANGELOG.md new file mode 100644 index 0000000..e97fea6 --- /dev/null +++ b/components/hermes/hermes-hook-audit/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 + +- Initial redacted Hermes shell-hook audit logger. diff --git a/components/hermes/hermes-hook-audit/CONTRIBUTING.md b/components/hermes/hermes-hook-audit/CONTRIBUTING.md new file mode 100644 index 0000000..dca65bb --- /dev/null +++ b/components/hermes/hermes-hook-audit/CONTRIBUTING.md @@ -0,0 +1,10 @@ +# Contributing + +Keep this hook observational. It should not block, rewrite, or mutate agent +behavior. + +Before opening a PR from the repository root, run: + +```bash +bash scripts/publish-check.sh +``` diff --git a/components/hermes/hermes-hook-audit/LICENSE b/components/hermes/hermes-hook-audit/LICENSE new file mode 100644 index 0000000..ba129f4 --- /dev/null +++ b/components/hermes/hermes-hook-audit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 RedBeret + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/components/hermes/hermes-hook-audit/README.md b/components/hermes/hermes-hook-audit/README.md new file mode 100644 index 0000000..ab8638e --- /dev/null +++ b/components/hermes/hermes-hook-audit/README.md @@ -0,0 +1,57 @@ +# hermes-hook-audit + +Redacted JSONL audit logger for Hermes shell hooks. + +This component records hook activity without changing agent behavior. It always +returns `{}` to Hermes and writes one JSON object per hook event. + +## What It Logs + +- UTC timestamp +- hook event name +- tool name +- session ID +- current working directory +- payload size +- command/result preview after redaction +- command SHA-256 when a terminal command is present + +It does not log raw secret values. Previews are capped by +`HERMES_HOOK_AUDIT_MAX_CHARS` and default to 300 characters. + +## Usage + +Install: + +```bash +bash setup.sh +``` + +Add it to Hermes hook config for events you want to observe: + +```yaml +hooks: + pre_tool_call: + - command: "~/.hermes/agent-hooks/audit-hook.sh" + timeout: 5 + post_tool_call: + - command: "~/.hermes/agent-hooks/audit-hook.sh" + timeout: 5 +``` + +Default log path: + +```text +~/.hermes/logs/hook-audit.jsonl +``` + +Override: + +```bash +HERMES_HOOK_AUDIT_LOG=/path/to/audit.jsonl bash audit-hook.sh +``` + +## Notes + +Store audit logs carefully. Even redacted logs may reveal project names, +commands, or workflow metadata. diff --git a/components/hermes/hermes-hook-audit/SECURITY.md b/components/hermes/hermes-hook-audit/SECURITY.md new file mode 100644 index 0000000..4b56e28 --- /dev/null +++ b/components/hermes/hermes-hook-audit/SECURITY.md @@ -0,0 +1,6 @@ +# Security + +Report issues through the repository's GitHub security advisory flow. + +This component is intentionally observational and must not block or mutate hook +behavior. Treat generated audit logs as sensitive operational metadata. diff --git a/components/hermes/hermes-hook-audit/audit-hook.sh b/components/hermes/hermes-hook-audit/audit-hook.sh new file mode 100755 index 0000000..1bdd91a --- /dev/null +++ b/components/hermes/hermes-hook-audit/audit-hook.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# hermes-hook-audit: redacted JSONL audit logger for Hermes shell hooks. + +set -euo pipefail + +payload="$(cat -)" +HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" +LOG_FILE="${HERMES_HOOK_AUDIT_LOG:-$HERMES_HOME/logs/hook-audit.jsonl}" +MAX_CHARS="${HERMES_HOOK_AUDIT_MAX_CHARS:-300}" + +mkdir -p "$(dirname "$LOG_FILE")" +chmod 700 "$(dirname "$LOG_FILE")" 2>/dev/null || true + +if ! command -v jq >/dev/null 2>&1; then + printf '{}\n' + exit 0 +fi + +if ! printf '%s' "$payload" | jq -e . >/dev/null 2>&1; then + printf '{}\n' + exit 0 +fi + +redact() { + sed -E \ + -e 's/sk-ant-[A-Za-z0-9_-]{20,}/[REDACTED]/g' \ + -e 's/sk-or-v1-[A-Za-z0-9_-]{20,}/[REDACTED]/g' \ + -e 's/nvapi-[A-Za-z0-9_-]{20,}/[REDACTED]/g' \ + -e 's/AKIA[0-9A-Z]{16}/[REDACTED]/g' \ + -e 's/ghp_[A-Za-z0-9]{36}/[REDACTED]/g' \ + -e 's/gho_[A-Za-z0-9]{36}/[REDACTED]/g' \ + -e 's/github_pat_[A-Za-z0-9_]{22,}/[REDACTED]/g' \ + -e 's/glpat-[A-Za-z0-9_-]{20,}/[REDACTED]/g' \ + -e 's/(Authorization:[[:space:]]*Bearer[[:space:]]+)[^[:space:]"'\'']+/\1[REDACTED]/Ig' \ + -e 's/(token|api[_-]?key|secret|password)=([^[:space:]&;]+)/\1=[REDACTED]/Ig' +} + +hash_text() { + if command -v sha256sum >/dev/null 2>&1; then + printf '%s' "$1" | sha256sum | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + printf '%s' "$1" | shasum -a 256 | awk '{print $1}' + else + printf '' + fi +} + +truncate_preview() { + local text="$1" + local max="$2" + printf '%s' "$text" | awk -v max="$max" '{ text = text $0 ORS } END { printf "%s", substr(text, 1, max) }' +} + +ts="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +event="$(printf '%s' "$payload" | jq -r '.hook_event_name // .event // empty')" +tool_name="$(printf '%s' "$payload" | jq -r '.tool_name // empty')" +session_id="$(printf '%s' "$payload" | jq -r '.session_id // empty')" +cwd="$(printf '%s' "$payload" | jq -r '.cwd // empty')" +command_text="$(printf '%s' "$payload" | jq -r '.tool_input.command // .args.command // empty')" +result_text="$(printf '%s' "$payload" | jq -r '.result // empty')" + +preview_source="$command_text" +preview_kind="command" +if [ -z "$preview_source" ] && [ -n "$result_text" ]; then + preview_source="$result_text" + preview_kind="result" +fi + +preview="$(printf '%s' "$preview_source" | redact)" +preview="$(truncate_preview "$preview" "$MAX_CHARS")" +command_hash="" +if [ -n "$command_text" ]; then + command_hash="$(hash_text "$command_text")" +fi +payload_bytes="$(printf '%s' "$payload" | wc -c | tr -d ' ')" + +jq -nc \ + --arg ts "$ts" \ + --arg event "$event" \ + --arg tool_name "$tool_name" \ + --arg session_id "$session_id" \ + --arg cwd "$cwd" \ + --arg preview_kind "$preview_kind" \ + --arg preview "$preview" \ + --arg command_hash "$command_hash" \ + --argjson payload_bytes "$payload_bytes" \ + '{ + ts: $ts, + event: $event, + tool_name: $tool_name, + session_id: $session_id, + cwd: $cwd, + payload_bytes: $payload_bytes, + preview_kind: $preview_kind, + preview: $preview, + command_sha256: $command_hash + }' >> "$LOG_FILE" + +chmod 600 "$LOG_FILE" 2>/dev/null || true +printf '{}\n' diff --git a/components/hermes/hermes-hook-audit/setup.sh b/components/hermes/hermes-hook-audit/setup.sh new file mode 100755 index 0000000..bd3cacd --- /dev/null +++ b/components/hermes/hermes-hook-audit/setup.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "hermes-hook-audit setup" +echo "-----------------------" + +command -v jq >/dev/null 2>&1 && echo "jq found" || { echo "jq required: brew install jq / apt install jq"; exit 1; } + +mkdir -p "$HERMES_HOME/agent-hooks" "$HERMES_HOME/logs" +cp "$SCRIPT_DIR/audit-hook.sh" "$HERMES_HOME/agent-hooks/audit-hook.sh" +chmod +x "$HERMES_HOME/agent-hooks/audit-hook.sh" + +echo "Installed audit-hook.sh to $HERMES_HOME/agent-hooks/" +echo "" +echo "Add to ~/.hermes/config.yaml:" +echo "" +echo "hooks:" +echo " pre_tool_call:" +echo " - command: \"~/.hermes/agent-hooks/audit-hook.sh\"" +echo " timeout: 5" +echo " post_tool_call:" +echo " - command: \"~/.hermes/agent-hooks/audit-hook.sh\"" +echo " timeout: 5" diff --git a/docs/COMPONENTS.md b/docs/COMPONENTS.md index a5e7302..e2285df 100644 --- a/docs/COMPONENTS.md +++ b/docs/COMPONENTS.md @@ -9,6 +9,7 @@ own README and setup script. - Safe terminal: stops commands likely to destroy data or leak credentials. - Commit guard: reminds the agent of commit rules and can block AI-attribution commit messages with a `commit-msg` hook. +- Hook audit: records redacted shell-hook activity for later review. ## Scanners and Gates diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index fc30629..7bf0282 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -91,4 +91,17 @@ fi grep -q 'Result: FAIL' /tmp/ask-mcp-bad.out ok "mcp guard" +audit_dir=$(mktemp -d) +audit_log="$audit_dir/hook-audit.jsonl" +audit_payload="{\"hook_event_name\":\"pre_tool_call\",\"tool_name\":\"terminal\",\"session_id\":\"s1\",\"cwd\":\"$audit_dir\",\"tool_input\":{\"command\":\"curl -H 'Authorization: Bearer ${fake_key}' https://example.com\"}}" +out=$(printf '%s\n' "$audit_payload" | HERMES_HOOK_AUDIT_LOG="$audit_log" bash components/hermes/hermes-hook-audit/audit-hook.sh) +[ "$out" = '{}' ] +[ -s "$audit_log" ] +if grep -q "$fake_key" "$audit_log"; then + echo "hook audit log leaked fake key" >&2 + exit 1 +fi +grep -q '\[REDACTED\]' "$audit_log" +ok "hook audit" + printf 'all smoke tests passed\n'