@fractal-ai/plugin-lifecycle-hooks
v0.2.0
Published
Enforced lifecycle hook gates for OpenClaw agent pipelines
Maintainers
Readme
@openclaw/plugin-lifecycle-hooks
⚠️ IMPORTANT — READ BEFORE USE
This plugin modifies how OpenClaw processes agent turns and can enforce security-sensitive policies (blocking commands, gating execution, injecting context). Misconfiguration may block your agent pipeline entirely.
Written by AI. While most hook actions and configurations have been live-tested in production, you should validate all critical tasks and security rules yourself before relying on them. Review your YAML config carefully — especially
blockactions — and test in a non-production environment first.
Enforced lifecycle hook gates for OpenClaw agent pipelines.
Define safety policies, observability rules, and execution gates in a simple YAML file. Hooks fire at every major pipeline transition — and actually block execution until they complete.
What It Does
OpenClaw's existing plugin hooks are fire-and-forget — the pipeline doesn't wait for them. This plugin replaces that with a gate engine: hooks block execution, complete their action, and only then allow the pipeline to continue.
This makes it possible to:
- ✅ Enforce hard safety policies (not just log them)
- ✅ Inject context before sub-agents start
- ✅ Push structured data to external systems at exactly the right moment
- ✅ Block dangerous commands before they execute
- ✅ Auto-log turns to persistent memory without manual intervention
Quick Start
1. Install the Plugin
npm install @openclaw/plugin-lifecycle-hooks2. Create HOOKS.yaml in Your Workspace Root
version: "1"
hooks:
# Block destructive rm commands (main agent + sub-agents)
- point: [turn:tool:pre, subagent:tool:pre]
match:
tool: exec
commandPattern: "^rm\\s"
action: block
onFailure:
action: block
notifyUser: true
message: "🚫 Use `trash <path>` instead of `rm`."
# Log all turns to per-topic JSONL files
- point: turn:post
match:
topicId: "*" # Match any topic
action: log
target: "memory/topics/topic-{topicId}.jsonl"
onFailure:
action: continue3. Restart the OpenClaw Gateway
openclaw gateway restartSee docs/QUICKSTART.md for detailed setup instructions.
Core Concepts
Hook Points
Every major transition in the OpenClaw pipeline is hookable:
| Hook Point | When It Fires | Example Use |
|------------|---------------|-------------|
| turn:pre | Prompt received, before agent starts | Log incoming prompt, inject context |
| turn:post | Agent response finalized | Summarize turn to topic file |
| turn:tool:pre | Before a tool call (main agent) | Block rm, require confirmation |
| turn:tool:post | After a tool call resolves | Log results, validate output |
| subagent:spawn:pre | Main agent about to spawn sub-agent | Inject shared context, log spawn |
| subagent:pre | Sub-agent starting its session | Apply sub-agent-specific rules |
| subagent:post | Sub-agent completed | Force write to log, relay results |
| subagent:tool:pre | Before a tool call (sub-agent) | Same rm guard as main agent |
| subagent:tool:post | After a sub-agent tool call | Audit log |
| heartbeat:pre | Before a heartbeat cycle | Prepare health check data |
| heartbeat:post | After a heartbeat cycle | Push metrics to dashboard |
| cron:pre | Before a cron job fires | Log job start, validate conditions |
| cron:post | After a cron job completes | Track success rates, alert failures |
Multiple points on one hook:
point: [turn:tool:pre, subagent:tool:pre]Actions Reference
🚫 block — Block Dangerous Operations
Immediately halt the pipeline with an optional message to the user.
Example: Block rm commands
hooks:
- point: [turn:tool:pre, subagent:tool:pre]
match:
tool: exec
commandPattern: "^rm\\s"
action: block
onFailure:
action: block
notifyUser: true
message: "🚫 Use `trash` instead of `rm`."When to use:
- Prevent destructive commands (
rm -rf,dd,chmod 777) - Enforce delegation policies (e.g., "npm install must run in sub-agent")
- Block privileged operations (
sudo,docker run --privileged)
Key fields:
action: block— Action typeonFailure.notifyUser— Send notification to user (optional)onFailure.message— Custom message (optional)
📝 log — Append Structured JSON to a Log File
Write structured JSON entries to a file or stdout. Supports variable interpolation in target.
Example: Per-topic turn logging
hooks:
- point: turn:post
match:
topicId: "*" # Match any topic
action: log
target: "memory/topics/topic-{topicId}.jsonl"
onFailure:
action: continueWhen to use:
- Auto-persist turns to per-topic memory files
- Audit log every exec call
- Track sub-agent spawns and completions
- Push structured data to external systems (parse JSONL with jq/Python)
Key fields:
action: log— Action typetarget— File path (supports{topicId},{sessionKey},{timestamp})- If
targetis"-"or omitted, logs to stdout
Output format:
{"point":"turn:post","sessionKey":"agent:main:telegram:group:-100EXAMPLE:topic:42","topicId":42,"timestamp":1735123456789,"prompt":"Fix the bug","response":"I'll investigate..."}📥 inject_context — Load Context from File into Agent Prompt
Load a file and inject its content into the session context. Supports variable interpolation in source.
Example: Inject topic context into every sub-agent
hooks:
- point: subagent:pre
match:
topicId: "*"
action: inject_context
source: "memory/topics/topic-{topicId}.jsonl"
onFailure:
action: continueWhen to use:
- Share workspace conventions (AGENTS.md, TOOLS.md) with every sub-agent
- Load per-topic context before each turn
- Inject security policies or style guides
Key fields:
action: inject_context— Action typesource— File path to load (supports variable interpolation)
Note: Currently a stub — will be wired to OpenClaw's context API in a future release.
🏷️ inject_origin — Inject Message Origin Metadata
Inject message origin metadata (chat ID, topic ID, sender) into the agent context.
Example: Add origin metadata to every turn
hooks:
- point: turn:pre
match:
topicId: "*"
action: inject_origin
onFailure:
action: continueWhen to use:
- Preserve origin context across sub-agent spawns
- Track which topic/chat/sender originated a request
- Survive context compaction
Key fields:
action: inject_origin— Action type
Injected metadata:
Origin: chat={chatId} topic={topicId} from={sender}🤖 summarize_and_log — LLM-Summarize Then Log
Use an LLM to summarize the event context, then append to a log file. Supports variable interpolation in target.
Example: Context catcher for topic memory
hooks:
- point: turn:post
match:
topicId: "*"
action: summarize_and_log
model: "anthropic/claude-haiku-4-5"
target: "memory/topics/topic-{topicId}.md"
onFailure:
action: continueWhen to use:
- Human-readable turn summaries for topic memory
- Condense long threads before context compaction
- Generate executive summaries for heartbeat/cron reports
Key fields:
action: summarize_and_log— Action typemodel— LLM model to use for summarizationtarget— File path (supports variable interpolation)
Output format: Appends markdown summary to the target file.
🔧 exec_script — Run a Shell Script with Optional Stdout Injection
Run a shell script; exit 0 = pass, non-zero = fail. Supports variable interpolation in script. Optionally captures stdout into agent context.
Example: Security check before tool calls
hooks:
- point: turn:tool:pre
match:
tool: exec
action: exec_script
script: "./scripts/security-check.sh {toolArgs.command}"
injectOutput: false
onFailure:
action: block
notifyUser: true
message: "⛔ Security check failed."Example: Inject script output into context
hooks:
- point: turn:pre
action: exec_script
script: "./scripts/load-env-vars.sh"
injectOutput: true
onFailure:
action: continueWhen to use:
- Run external validators (security scanners, linters)
- Push webhook notifications (Slack, Discord, PagerDuty)
- Dynamically load environment-specific config
Key fields:
action: exec_script— Action typescript— Path to shell script (supports variable interpolation)injectOutput— Iftrue, capture stdout and inject into context (default:false)
📢 notify_user — Send Telegram Notification
Send a fire-and-forget Telegram notification to the user. Optionally generates an LLM summary before notifying.
Example: Notify when sub-agent completes
hooks:
- point: subagent:post
action: notify_user
model: "anthropic/claude-haiku-4-5" # Optional: enables LLM summary
onFailure:
action: continueWhen to use:
- Alert on critical events (sub-agent completion, dangerous command blocked)
- Push real-time status updates to Telegram
- Human-in-the-loop confirmations
Key fields:
action: notify_user— Action typemodel— LLM model to use for summarization (optional)
Note: The notification target is automatically determined from the session context or defaults.notificationTarget in the config.
Matching
Filters narrow when a hook fires. All specified fields must match (AND logic):
match:
tool: exec # Exact tool name
commandPattern: "^rm\\s" # Regex: command, path, url, or prompt
topicId: 42 # Forum topic ID (or "*" for any topic)
isSubAgent: false # true = sub-agent only, false = main only
sessionPattern: "telegram:" # Regex against full session key
custom: ./matchers/my.js # Path to JS module returning booleanSpecial matchers:
topicId: "*"— Matches any session with atopicIdfield (useful for "all topics" hooks)topicId: 42— Matches only topic 42
commandPattern inspects toolArgs.command → toolArgs.path → toolArgs.url → toolArgs.message → context.prompt in order.
Variable Interpolation
All path-based actions (log, summarize_and_log, inject_context, exec_script) support variable interpolation:
| Variable | Example Value | Use Case |
|----------|---------------|----------|
| {topicId} | 42 | Per-topic log files: memory/topics/topic-{topicId}.md |
| {sessionKey} | agent:main:telegram:group:-100EXAMPLE:topic:42 | Session-specific logs |
| {timestamp} | 1735123456789 | Timestamped snapshots |
Example:
- point: turn:post
match:
topicId: "*" # Match any topic
action: log
target: "memory/topics/topic-{topicId}.jsonl"This creates topic-42.jsonl, topic-43.jsonl, etc. automatically based on the session's topic ID.
Error Handling — onFailure
Configure per-hook what happens when an action fails or a gate doesn't pass:
onFailure:
action: block # block | retry | notify | continue
retries: 3 # (retry only) max attempts before giving up
notifyUser: true # surface message to the user
message: "Custom failure message"| action | Behavior |
|----------|----------|
| block | Return passed: false, halt pipeline |
| retry | Retry with exponential backoff (100ms → 200ms → 400ms...) |
| notify | Send a Telegram notification to the user and continue (fire-and-forget) |
| continue | Log error, return passed: true, pipeline continues |
Default if onFailure is omitted: { action: "continue" }.
Example Configurations
Ready-to-use configs in examples/:
| File | What it does |
|------|--------------|
| rm-guard.yaml | Block rm, shred, rmdir — main agent and sub-agents |
| topic-logging.yaml | Auto-summarize turns to per-topic memory files |
| subagent-context.yaml | Inject AGENTS.md + topic context into every sub-agent |
| heartbeat-dashboard.yaml | Log heartbeats and push to external dashboard via script |
| notification-webhook.yaml | Slack/Discord webhook on spawns, completions, and blocks |
| kitchen-sink.yaml | Every hook point and option, fully annotated |
| security.hooks.yaml | Block rm, sudo, dd, chmod 777, curl-pipe-to-shell |
| logging.hooks.yaml | Comprehensive turn/tool/subagent/cron logging |
| delegation.hooks.yaml | Enforce sub-agent delegation for npm, builds, Docker |
To use an example:
cp node_modules/@openclaw/plugin-lifecycle-hooks/examples/rm-guard.yaml HOOKS.yaml
# Edit paths and options to match your workspace
openclaw gateway restartDocumentation
| Doc | What's in it | |-----|-------------| | QUICKSTART.md | Step-by-step: install → HOOKS.yaml → verify | | CONFIGURATION.md | Complete field reference for all options | | WALKTHROUGHS.md | 5 use-case walkthroughs with full configs | | ARCHITECTURE.md | Engine internals, data flow, extension points |
Development
# Install dependencies
npm install
# Build TypeScript → dist/
npm run build
# Run all tests
npm test
# Watch mode
npm run test:watch
# Type-check only (no emit)
npm run lintProject Structure
src/
├── types.ts All TypeScript types and interfaces
├── config.ts HOOKS.yaml parser + schema validation
├── matcher.ts Match filter evaluation
├── engine.ts LifecycleGateEngine — orchestrates hook execution
├── notify.ts Fire-and-forget Telegram user notifications
├── context-store.ts Origin context storage for inject_origin
├── llm.ts LLM completion wrapper
├── template.ts Variable interpolation helpers
├── index.ts Public exports
├── actions/
│ ├── block.ts Block action
│ ├── log.ts JSONL log action
│ ├── summarize.ts LLM summarize + log action
│ ├── inject.ts Context injection action
│ ├── inject-origin.ts Origin metadata injection
│ ├── exec-script.ts Shell script action
│ ├── notify-action.ts User notification action
│ └── index.ts Action registry and dispatcher
└── utils/
└── interpolate.ts Path variable interpolationTypeScript API
LifecycleGateEngine
import { LifecycleGateEngine } from '@openclaw/plugin-lifecycle-hooks';
const engine = new LifecycleGateEngine();
await engine.loadConfig('./HOOKS.yaml');Key methods:
// Load (and validate) a HOOKS.yaml file
engine.loadConfig(path: string): Promise<HooksConfig>
// Hot-reload from the same path (no restart required)
engine.reloadConfig(): Promise<HooksConfig | null>
// Execute all matching hooks at a pipeline point
engine.execute(point: HookPoint, context: HookContext): Promise<HookResult[]>
// Get enabled hooks for a point (without match evaluation)
engine.getHooksForPoint(point: HookPoint): HookDefinition[]
// Build a HookContext with required fields pre-filled
LifecycleGateEngine.buildContext(point, sessionKey, overrides): HookContextTypes
// All valid hook points
type HookPoint =
| 'turn:pre' | 'turn:post'
| 'turn:tool:pre' | 'turn:tool:post'
| 'subagent:spawn:pre' | 'subagent:pre' | 'subagent:post'
| 'subagent:tool:pre' | 'subagent:tool:post'
| 'heartbeat:pre' | 'heartbeat:post'
| 'cron:pre' | 'cron:post';
// Context passed to every hook at runtime
interface HookContext {
point: HookPoint;
sessionKey: string;
topicId?: number | string;
prompt?: string;
toolName?: string;
toolArgs?: Record<string, unknown>;
response?: string;
subagentLabel?: string;
cronJob?: string;
heartbeatMeta?: Record<string, unknown>;
raw?: Record<string, unknown>;
timestamp: number;
}
// Result from a hook execution
interface HookResult {
passed: boolean; // false = block the pipeline
action: HookAction;
message?: string;
duration: number; // ms
}All types are exported from the package root:
import type {
HookPoint,
HookContext,
HookResult
} from '@openclaw/plugin-lifecycle-hooks';License
MIT © OpenClaw Contributors
See LICENSE for full text.
