npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@coffeexdev/openclaw-sentinel

v0.8.3

Published

Secure declarative gateway-native watcher plugin for OpenClaw

Readme

@coffeexdev/openclaw-sentinel

Secure, declarative, gateway-native background watcher plugin for OpenClaw.

Quick start (OpenClaw users)

If you only read one section, read this.

1) Install plugin

openclaw plugins install @coffeexdev/openclaw-sentinel

2) Configure Sentinel

Add/update ~/.openclaw/openclaw.json:

{
  plugins: {
    entries: {
      "openclaw-sentinel": {
        enabled: true,
        config: {
          // Required: watchers can only call endpoints on these hosts.
          allowedHosts: ["api.github.com", "api.coingecko.com"],

          // Default dispatch base for internal webhook callbacks.
          localDispatchBase: "http://127.0.0.1:18789",

          // Optional: base prefix for isolated /hooks/sentinel callback sessions.
          // Sentinel appends :watcher:<id> by default (or :group:<key> when grouped).
          hookSessionPrefix: "agent:main:hooks:sentinel",

          // Optional: default group key for callbacks without explicit hookSessionGroup.
          // hookSessionGroup: "ops-alerts",

          // Optional: suppress duplicate relays by dedupe key within this time window.
          hookRelayDedupeWindowMs: 120000,

          // Optional: guarantee hook-response delivery contract for /hooks/sentinel callbacks.
          // Wait this long for assistant-authored output before fallback behavior applies.
          hookResponseTimeoutMs: 30000,

          // Optional: timeout fallback relay mode for /hooks/sentinel response contracts.
          // "none" = no fallback message, "concise" = send a fail-safe relay line.
          hookResponseFallbackMode: "concise",

          // Optional: dedupe repeated callback response contracts by dedupe key.
          hookResponseDedupeWindowMs: 120000,

          // Optional: payload style for non-/hooks/sentinel deliveryTargets notifications.
          // "none" suppresses delivery-target message fan-out (callback still fires).
          // "concise" (default) sends human-friendly relay text only.
          // "debug" appends a structured sentinel envelope block for diagnostics.
          // notificationPayloadMode: "concise",

          // Optional: max length for watcher.fire.operatorGoal.
          // Default: 12000, min: 500, hard cap: 20000.
          // Keep this as small as practical to reduce state + prompt bloat.
          // maxOperatorGoalChars: 12000,

          // Optional legacy alias for hookSessionPrefix (still supported).
          // hookSessionKey: "agent:main:hooks:sentinel",

          // Optional: explicit bearer token override for dispatch calls back to gateway.
          // Sentinel auto-detects gateway auth token from runtime config when available,
          // so manual token copy is usually no longer required.
          // dispatchAuthToken: "<gateway-token>"
        },
      },
    },
  },
}

3) Restart gateway

openclaw gateway restart

Troubleshooting: Unrecognized key: "sentinel"

If gateway startup/validation reports:

Unrecognized key: "sentinel"

your config is using the old root-level shape. Move Sentinel config under:

  • plugins.entries.openclaw-sentinel.config

Sentinel also logs a runtime warning when that legacy root key is still observable, but it never writes a root-level sentinel key.

Hardening notes (0.6 minor)

  • hookSessionKey remains supported but is deprecated. If both are present, hookSessionPrefix now wins.
  • HTTP watcher strategies now set redirect: "error" to prevent host-allowlist bypass via redirects.
  • Watcher IDs are now constrained to ^[A-Za-z0-9_-]{1,128}$.
  • /hooks/sentinel validates JSON Content-Type when provided and returns 415 for unsupported media types.

4) Create your first watcher (sentinel_control)

{
  "action": "create",
  "watcher": {
    "id": "eth-price-watch",
    "skillId": "skills.alerts",
    "enabled": true,
    "strategy": "http-poll",
    "endpoint": "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd",
    "intervalMs": 15000,
    "match": "all",
    "conditions": [{ "path": "ethereum.usd", "op": "gte", "value": 5000 }],
    "fire": {
      "webhookPath": "/hooks/sentinel",
      "eventName": "eth_target_hit",
      "intent": "price_threshold_review",
      "contextTemplate": {
        "asset": "ETH",
        "priceUsd": "${payload.ethereum.usd}",
        "workflow": "alerts"
      },
      "priority": "high",
      "sessionGroup": "portfolio-risk",
      "operatorGoal": "Confirm threshold breach, summarize impact, and notify on-call with actionable next steps.",
      "deadlineTemplate": "${timestamp}",
      "payloadTemplate": {
        "event": "${event.name}",
        "price": "${payload.ethereum.usd}",
        "ts": "${timestamp}"
      }
    },
    "retry": { "maxRetries": 5, "baseMs": 500, "maxMs": 15000 },
    "fireOnce": true
  }
}

5) Verify

Use sentinel_control:

{ "action": "list" }
{ "action": "status", "id": "eth-price-watch" }

What happens when a watcher fires?

  1. Sentinel evaluates conditions.
  2. On match, it dispatches a generic callback envelope (type: "sentinel.callback") to localDispatchBase + webhookPath.
  3. The envelope includes stable keys (intent, context, watcher, trigger, bounded payload, deliveryTargets, deliveryContext, source) so downstream agent behavior is workflow-agnostic.
  4. For /hooks/sentinel, Sentinel enqueues an instruction-prefixed system event with a structured callback prompt context (watcher, trigger, source, deliveryTargets, deliveryContext, context, payload) plus the full envelope, then requests an immediate cron:sentinel-callback wake (avoids heartbeat-poll prompting).
  5. The hook route creates a response-delivery contract keyed by callback dedupe key, preserving original chat/session context (deliveryContext) and intended relay targets.
  6. OpenClaw processes each callback in an isolated hook session: per-watcher by default, or grouped when hookSessionGroup / fire.sessionGroup is set. Shared global hook-session mode is intentionally not supported.
  7. Relay guardrails suppress control-token outputs (NO_REPLY, HEARTBEAT_OK, empty variants). If model output is unusable, Sentinel emits a concise contextual fallback message. Timeout fallback behavior still follows hookResponseFallbackMode.

The /hooks/sentinel route is auto-registered on plugin startup (idempotent). Response contracts are dedupe-aware by callback dedupe key (hookResponseDedupeWindowMs).

Sample emitted envelope:

{
  "type": "sentinel.callback",
  "version": "1",
  "intent": "price_threshold_review",
  "actionable": true,
  "watcher": {
    "id": "eth-price-watch",
    "skillId": "skills.alerts",
    "eventName": "eth_target_hit",
    "intent": "price_threshold_review",
    "strategy": "http-poll",
    "endpoint": "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd",
    "match": "all",
    "conditions": [{ "path": "ethereum.usd", "op": "gte", "value": 5000 }],
    "fireOnce": false
  },
  "trigger": {
    "matchedAt": "2026-03-04T15:00:00.000Z",
    "dedupeKey": "<sha256>",
    "priority": "high"
  },
  "context": { "asset": "ETH", "priceUsd": 5001, "workflow": "alerts" },
  "payload": { "ethereum": { "usd": 5001 } },
  "deliveryTargets": [{ "channel": "telegram", "to": "5613673222" }],
  "deliveryContext": {
    "sessionKey": "agent:main:telegram:direct:5613673222",
    "messageChannel": "telegram",
    "requesterSenderId": "5613673222",
    "currentChat": { "channel": "telegram", "to": "5613673222" }
  },
  "source": { "plugin": "openclaw-sentinel", "route": "/hooks/sentinel" }
}

fire.operatorGoal length guidance

  • Default max length is 12000 chars (raised from 500).
  • You can tune this with plugin config: maxOperatorGoalChars (min 500, hard cap 20000).
  • Recommendation: keep most goals in the 200-2000 char range for clarity and lower prompt/state overhead.
  • Use larger goals only when you genuinely need richer policy/runbook context.

Tradeoff: larger values improve callback guidance but also increase persisted watcher size and callback prompt footprint, so unbounded values are intentionally not allowed.

Migration: existing watchers under the old 500-char limit continue to work unchanged. No migration action is required unless you want to add richer guidance.

Why Sentinel

Sentinel runs watcher lifecycles inside the gateway with fixed strategies and declarative conditions. It does not execute user-authored code from watcher definitions.

Features

  • Tool registration: sentinel_control
    • actions: create (add), enable, disable, remove (delete), status (get), list
  • Strict schema validation (TypeBox, strict object checks) + code-like field/value rejection
  • Strategies:
    • http-poll (supports POST with custom body for JSON-RPC etc.)
    • websocket
    • sse
    • http-long-poll
    • evm-call — smart contract state polling via eth_call with ABI encoding/decoding
  • Condition operators:
    • eq, neq, gt, gte, lt, lte, exists, absent, contains, matches, changed
  • Match mode: all / any
  • Fire templating: substitution-only placeholders, non-Turing-complete
  • Local webhook dispatch model (no outbound custom fire URL)
  • Default callback route: /hooks/sentinel (auto-registered)
  • Persistence: ~/.openclaw/sentinel-state.json
  • Resource limits and per-skill limits
  • allowedHosts endpoint enforcement
  • CLI surface: list, status, enable, disable, audit

Tool input example (sentinel_control:create)

{
  "action": "create",
  "watcher": {
    "id": "sentinel-alert",
    "skillId": "skills.general-monitor",
    "enabled": true,
    "strategy": "http-poll",
    "endpoint": "https://api.github.com/events",
    "intervalMs": 15000,
    "match": "any",
    "conditions": [{ "path": "type", "op": "eq", "value": "PushEvent" }],
    "fire": {
      "webhookPath": "/hooks/sentinel",
      "eventName": "sentinel_push",
      "payloadTemplate": {
        "watcher": "${watcher.id}",
        "event": "${event.name}",
        "type": "${payload.type}",
        "ts": "${timestamp}"
      }
    },
    "retry": { "maxRetries": 5, "baseMs": 250, "maxMs": 5000 },
    "deliveryTargets": [
      { "channel": "telegram", "to": "5613673222" },
      { "channel": "discord", "to": "123456789012345678", "accountId": "main" }
    ]
  }
}

deliveryTargets is optional. If omitted on create, Sentinel infers a default target from the current tool/session context (channel + current peer).

Notification payload delivery modes

Sentinel always dispatches the callback envelope to localDispatchBase + webhookPath on match. notificationPayloadMode only controls additional fan-out messages to deliveryTargets for watcher dispatches (for example /hooks/agent). It does not control /hooks/sentinel hook-response contracts or assistant-output relay behavior.

Global mode options:

  • none: suppress delivery-target notification messages (callback dispatch still occurs)
  • concise (default): send short relay text only
  • debug: send relay text plus SENTINEL_DEBUG_ENVELOPE_JSON block

1) Global notifications disabled (none)

{
  plugins: {
    entries: {
      "openclaw-sentinel": {
        enabled: true,
        config: {
          allowedHosts: ["api.github.com"],
          notificationPayloadMode: "none",
        },
      },
    },
  },
}

2) Global concise relay (default)

{
  plugins: {
    entries: {
      "openclaw-sentinel": {
        enabled: true,
        config: {
          allowedHosts: ["api.github.com"],
          notificationPayloadMode: "concise",
        },
      },
    },
  },
}

3) Global debug diagnostics

{
  plugins: {
    entries: {
      "openclaw-sentinel": {
        enabled: true,
        config: {
          allowedHosts: ["api.github.com"],
          notificationPayloadMode: "debug",
        },
      },
    },
  },
}

In debug mode, delivery notifications include the same concise relay line plus a SENTINEL_DEBUG_ENVELOPE_JSON block for diagnostics.

4) Per-watcher override (watcher.fire.notificationPayloadMode)

{
  "action": "create",
  "watcher": {
    "id": "status-watch",
    "skillId": "skills.ops",
    "enabled": true,
    "strategy": "http-poll",
    "endpoint": "https://status.example.com/api/health",
    "intervalMs": 10000,
    "match": "all",
    "conditions": [{ "path": "status", "op": "eq", "value": "degraded" }],
    "fire": {
      "webhookPath": "/hooks/agent",
      "eventName": "service_degraded",
      "notificationPayloadMode": "none",
      "payloadTemplate": { "event": "${event.name}", "status": "${payload.status}" }
    },
    "retry": { "maxRetries": 5, "baseMs": 250, "maxMs": 5000 }
  }
}

Allowed values:

  • inherit (or omitted): follow global notificationPayloadMode
  • none: suppress delivery-target notification messages for this watcher
  • concise: force concise notification text for this watcher
  • debug: force debug envelope output for this watcher

Precedence: watcher override > global setting.

Migration notes

  • Existing installs keep default behavior (concise) unless you set notificationPayloadMode explicitly.
  • If you want callback-only operation (wake LLM loop via /hooks/sentinel but no delivery-target chat message), set global or per-watcher mode to none.

Hook-response delivery contract (/hooks/sentinel)

/hooks/sentinel now enforces a dedicated trigger → LLM → user-visible relay contract:

  1. Callback is enqueued to isolated hook session.
  2. Contract captures original delivery context (deliveryContext + resolved deliveryTargets).
  3. The LLM calls sentinel_act notify to deliver results to targets (sole delivery mechanism).
  4. Any successful sentinel_act call fulfills the relay contract and cancels the timeout timer.
  5. If no sentinel_act call arrives in time (hookResponseTimeoutMs), timeout fallback is configurable:
    • hookResponseFallbackMode: "concise" (default) sends a short fail-safe relay.
    • hookResponseFallbackMode: "none" suppresses timeout fallback.
  6. Repeated callbacks with same dedupe key are idempotent within hookResponseDedupeWindowMs.

Example config:

{
  plugins: {
    entries: {
      "openclaw-sentinel": {
        enabled: true,
        config: {
          allowedHosts: ["api.github.com"],
          hookResponseTimeoutMs: 30000,
          hookResponseFallbackMode: "concise",
          hookResponseDedupeWindowMs: 120000,
        },
      },
    },
  },
}

Runtime controls

{ "action": "status", "id": "sentinel-alert" }
{ "action": "disable", "id": "sentinel-alert" }
{ "action": "remove", "id": "sentinel-alert" }

JSON Schema

Formal JSON Schema for sentinel config/watchers:

  • schema/sentinel.schema.json

Documentation

CLI

openclaw-sentinel list
openclaw-sentinel status <watcher-id>
openclaw-sentinel enable <watcher-id>
openclaw-sentinel disable <watcher-id>
openclaw-sentinel audit

Development

pnpm install
pnpm run lint
pnpm test
pnpm run build

License

MIT