Integrations/OpenClaw
Live

GuardEntry ร— OpenClaw

Protect your OpenClaw agents with GuardEntry compliance policies. Every tool call is evaluated before it executes โ€” blocking violations, flagging drift, and building an audit trail automatically.

Server / Docker setupNo Node.js required

Running OpenClaw on a Linux server or inside a Docker container? Use the standalone bash hook โ€” just curl and bash, no npm needed.

1. Download the hook script

curl -fsSL https://app.guardentry.ai/fastgrc-hook.sh -o /usr/local/bin/guardentry-hook && chmod +x /usr/local/bin/guardentry-hook

This places the hook at /usr/local/bin/guardentry-hook โ€” on the system PATH and ready to reference in the next step.

Can't reach the URL? Create the file manually โ€บ

Copy the script below, paste it into a new file named guardentry-hook, then chmod +x guardentry-hook.

#!/usr/bin/env bash
# guardentry-hook โ€” GuardEntry PreToolUse compliance hook (no Node.js required)
set -uo pipefail

API_KEY="${GUARDENTRY_API_KEY:-}"
BASE_URL="${GUARDENTRY_BASE_URL:-https://app.guardentry.ai}"
POLICY_ID="${GUARDENTRY_POLICY_ID:-}"

[[ -z "$API_KEY" ]] && exit 0

INPUT="$(cat 2>/dev/null || true)"
[[ -z "$INPUT" ]] && exit 0

_json_str() {
  local expr="$1" input="$2"
  if command -v jq &>/dev/null; then
    printf '%s' "$input" | jq -r "$expr // empty" 2>/dev/null || true
  elif command -v python3 &>/dev/null; then
    printf '%s' "$input" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    print($expr, end='')
except Exception:
    pass
" 2>/dev/null || true
  fi
}

TOOL_NAME="$(_json_str '.tool_name' "$INPUT")"
AGENT_ID="$(_json_str '.session_id // .agent_id // ""' "$INPUT")"

if command -v jq &>/dev/null; then
  TOOL_INPUT="$(printf '%s' "$INPUT" | jq -c '.tool_input // {}' 2>/dev/null || echo '{}')"
elif command -v python3 &>/dev/null; then
  TOOL_INPUT="$(printf '%s' "$INPUT" | python3 -c "
import sys, json
try:
    d = json.load(sys.stdin)
    print(json.dumps(d.get('tool_input', {})), end='')
except Exception:
    print('{}', end='')
" 2>/dev/null || echo '{}')"
else
  exit 0
fi

TOOL_NAME="${TOOL_NAME:-unknown}"
TOOL_INPUT="${TOOL_INPUT:-{}}"
CONTENT_ESC="$(printf '%s' "tool_name: ${TOOL_NAME}\nargs: ${TOOL_INPUT}" | sed 's/\\/\\\\/g; s/"/\\"/g')"

BODY="{\"subjectContent\":\"$CONTENT_ESC\",\"subjectType\":\"tool_argument\",\"direction\":\"ingress\",\"agentId\":\"$AGENT_ID\""
[[ -n "$POLICY_ID" ]] && BODY+=",\"policyId\":\"$POLICY_ID\""
BODY+="}"

RESPONSE="$(curl -sf --max-time 5 \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d "$BODY" \
  "${BASE_URL}/api/v1/policy-router/evaluate" 2>/dev/null)" || true

[[ -z "$RESPONSE" ]] && exit 0

if command -v jq &>/dev/null; then
  DECISION="$(printf '%s' "$RESPONSE" | jq -r '.decision // "allow"' 2>/dev/null || echo 'allow')"
  REASONING="$(printf '%s' "$RESPONSE" | jq -r '.reasoning // ""' 2>/dev/null || true)"
  MATCHED="$(printf '%s' "$RESPONSE" | jq -r '.policyContext.matchedRule // ""' 2>/dev/null || true)"
  OVERRIDE_ACTIVE="$(printf '%s' "$RESPONSE" | jq -r '(.reasonTags // []) | contains(["override_block_active"])' 2>/dev/null || echo 'false')"
elif command -v python3 &>/dev/null; then
  DECISION="$(printf '%s' "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('decision','allow'),end='')" 2>/dev/null || echo 'allow')"
  REASONING="$(printf '%s' "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('reasoning',''),end='')" 2>/dev/null || true)"
  MATCHED="$(printf '%s' "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print((d.get('policyContext') or {}).get('matchedRule',''),end='')" 2>/dev/null || true)"
  OVERRIDE_ACTIVE="$(printf '%s' "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); tags=d.get('reasonTags') or []; print('true' if 'override_block_active' in tags else 'false',end='')" 2>/dev/null || echo 'false')"
else
  exit 0
fi

if [[ "${DECISION:-allow}" == "block" || "${DECISION:-allow}" == "require_approval" ]]; then
  [[ -n "${MATCHED:-}" ]] && printf 'GuardEntry blocked: [%s] %s\n' "$MATCHED" "${REASONING:-action blocked}" >&2 || printf 'GuardEntry blocked: %s\n' "${REASONING:-action blocked}" >&2
  exit 2
fi

# Observability mode: block was downgraded to allow โ€” surface the diagnostic
if [[ "${OVERRIDE_ACTIVE:-false}" == "true" ]]; then
  [[ -n "${MATCHED:-}" ]] && printf '[GuardEntry] Observability mode โ€” would have blocked: [%s] %s\n' "$MATCHED" "${REASONING:-}" >&2 || [[ -n "${REASONING:-}" ]] && printf '[GuardEntry] Observability mode โ€” would have blocked: %s\n' "$REASONING" >&2
fi

exit 0

1b. Webchat exec approvals โ€” socket server

The bash hook covers tool calls from Claude Code and OpenClaw's hook runner. To also block webchat exec commands โ€” shell commands run directly from the OpenClaw web chat โ€” you need the exec-approvals socket server.

OpenClaw's plugin installer has an ownership conflict in Docker (installs as uid=1000 but requires uid=0 to load). Use guardentry-hook serve-approvals as a standalone process on the host instead.

Step 1 โ€” Configure exec-approvals inside the container (run once):

docker exec <container> guardentry-hook setup \
  --api-key fgrc_k1_your_key_here \
  --policy-id fgrc_p1_your_policy_id

Step 2 โ€” Create a systemd service on the host:

# /etc/systemd/system/guardentry-approvals.service
[Unit]
Description=GuardEntry Exec Approvals Server
After=docker.service
Requires=docker.service

[Service]
Type=simple
Restart=always
RestartSec=15
ExecStartPre=/bin/bash -c 'until docker exec <container> echo ok 2>/dev/null; do sleep 2; done'
ExecStart=/usr/bin/docker exec <container> guardentry-hook serve-approvals

[Install]
WantedBy=multi-user.target
systemctl daemon-reload && systemctl enable --now guardentry-approvals

The service polls until the container is ready, then starts the socket server. It auto-recovers after docker restart. Verify: docker exec <container> ls /data/.openclaw/exec-approvals.sock โ€” should show srw------- 1 root root.

2a. Configure via Claude Code (~/.claude/settings.json)

If your agent runs inside Claude Code, embed your API key directly in the command string. This ensures the hook works regardless of how Claude Code was started (terminal, service, GUI):

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "GUARDENTRY_API_KEY=fgrc_k1_your_key_here /usr/local/bin/guardentry-hook"
          }
        ]
      }
    ]
  }
}

Get your API key free at guardentry.ai/connect. Optional: append GUARDENTRY_POLICY_ID=your_id before the path to target a specific policy.

Restart Claude Code after saving. The hook is fail-open โ€” if the API is unreachable or times out, the tool call proceeds normally.

2b. Configure via OpenClaw HOOK.md (v2026.3.28+)

If your agent runs under OpenClaw's own hook runner, embed the key in the handler field of your HOOK.md. OpenClaw spawns hook handlers as subprocesses โ€” they do not inherit your shell's exported variables, so the key must be inlined:

---
name: GuardEntry Policy Check
description: Evaluate every tool call against your GuardEntry compliance policy
hooks:
  - matcher: PreToolUse
    handler: "GUARDENTRY_API_KEY=fgrc_k1_your_key_here guardentry-hook"
---

If your OpenClaw version doesn't support inline env vars in the handler string, use a wrapper script instead:

#!/usr/bin/env bash
# guardentry-wrapper.sh โ€” place alongside your HOOK.md
GUARDENTRY_API_KEY=fgrc_k1_your_key_here exec guardentry-hook "$@"
handler: "./guardentry-wrapper.sh"

3. Test it

Run this to confirm the hook fires and reaches the API. Replace the key with yours:

echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp"},"session_id":"test-123"}' \
  | GUARDENTRY_API_KEY=fgrc_k1_your_key_here /usr/local/bin/guardentry-hook
echo "Exit: $?"
# Exit 0 = allowed  |  Exit 2 = blocked

Exit 2 = working. Check your dashboard audit tab for the logged decision. If exit is always 0 with no audit entry, the key is wrong or the hook isn't installed at /usr/local/bin/guardentry-hook.

Hook connects but nothing is blocked? Your policy starts in Observability Mode โ€” violations are logged but not blocked. Enable enforcement in the dashboard when ready.

OpenClaw HOOK.md

The recommended OpenClaw integration uses OpenClaw's native hook runner. Add guardentry-hook as a handler in your HOOK.md โ€” OpenClaw spawns it as a subprocess before every tool call.

1. Install the binary

npm install -g guardentry-openclaw

This puts guardentry-hook on your system PATH.

2. Save your API key and policy (one-time)

guardentry-hook set-key fgrc_k1_your_key_here
guardentry-hook set-policy fgrc_p1_your_policy_id

Both are saved to ~/.guardentry.json โ€” the binary reads them automatically. You get both values from the /connect success screen.

Verify: guardentry-hook get-key ยท guardentry-hook get-policy ยท To switch policy later: guardentry-hook set-policy <new-id> (find IDs in the dashboard). Policy ID is optional โ€” omit to use your org-wide default.

3. Auto-configure HOOK.md

From your OpenClaw project directory:

guardentry-hook install-hook

Creates or updates HOOK.md with the correct handler entry โ€” idempotent, safe to re-run. Pass a path if your project is elsewhere: guardentry-hook install-hook /path/to/project

Restart OpenClaw โ€” the hook fires for every tool call from that point on.

4. Verify

Run a query in OpenClaw chat, then check your audit log โ€” every tool call should appear within seconds.

Nothing showing? Confirm guardentry-hook get-key returns your key and that guardentry-hook is on PATH (which guardentry-hook).

Policy options at install time

When you sign up at /connect, you choose one of three default policies โ€” all start in Observability Mode (log only, nothing blocked):

PolicyBest for
General ObserverAny agent โ€” just start logging to understand behavior
Compliance Drift MonitorSOC 2 / ISO 27001 โ€” detect policy drift as agents evolve
SOC 2 AuditorAudit-ready agents โ€” strict read-only with evidence collection

Switch to enforcement from your dashboard when ready.

What gets evaluated

Every tool call sends:

{
  "subjectContent": "tool_name: delete_record\nargs: {\"table\":\"users\",\"id\":\"123\"}",
  "subjectType": "tool_argument",
  "direction": "ingress",
  "agentId": "<session id>"
}

The GuardEntry policy router checks this against your configured rules and returns a decision in ~20โ€“50ms. Exit 0 = allow. Exit 2 = block (reason written to stderr, shown in OpenClaw chat).

Advanced: TypeScript API

For custom agent wrappers or environments where you can't use HOOK.md, guardentry-openclaw also exports a programmatic API:

npm install guardentry-openclaw
import { evaluate, GuardEntryBlockedError } from 'guardentry-openclaw';

// Evaluate a tool call manually (e.g. inside your own hook middleware)
const result = await evaluate({
  toolName: 'delete_record',
  args: { table: 'users', id: '123' },
  apiKey: 'fgrc_k1_your_key_here',   // or omit to use ~/.guardentry.json
});

if (result?.decision === 'block') {
  throw new GuardEntryBlockedError(
    result.policyContext?.matchedRule ?? 'policy rule',
    result.reasoning
  );
}

The evaluate() function is the same one used by the hook binary โ€” fail-open on timeout or network error.

Safety guarantees

  • โœ“Fail-open: If GuardEntry is unreachable or times out, the tool call is allowed through with a console.warn. GuardEntry never breaks your agent due to infra issues.
  • โœ“No data stored: Tool arguments are evaluated in-memory. Only the decision record (tool name, decision, reasoning) is stored in the audit log โ€” not the full argument payload.