@clawhub-kintupercy-b4b0c00bd2
Spending cap proxy for OpenClaw. Enforce hard daily and monthly limits across all your AI models (Claude, GPT, Gemini, and more) under one cap. Stop runaway...
---
name: clawcap
description: Spending cap proxy for OpenClaw. Enforce hard daily and monthly limits across all your AI models (Claude, GPT, Gemini, and more) under one cap. Stop runaway token burn, detect heartbeat loops, and kill agents remotely via Telegram. One URL, one cap, all models tracked together.
version: 1.1.2
metadata:
openclaw:
requires:
env:
- CLAWCAP_TOKEN
bins:
- node
primaryEnv: CLAWCAP_TOKEN
emoji: "\U0001F6E1"
homepage: https://clawcap.co
---
# ClawCap — Spending Cap for OpenClaw
Stop runaway API bills. ClawCap enforces hard spending caps across every model your agents use — Claude, GPT, Gemini, and more — under a single proxy URL.
## What It Does
- **Hard daily & monthly caps** — your agents stop when you hit your limit, not when your wallet is empty
- **All models, one cap** — Claude, GPT, Gemini, DeepSeek, Mistral, and more tracked together
- **Heartbeat detection** — catches agents stuck in polling loops burning tokens
- **Loop detection** — stops agents repeating the same failed request
- **Kill switch** — stop any agent instantly via Telegram or API
- **Real-time tracking** — see exactly what you're spending per model
## Setup (2 minutes)
### 1. Get Your Token
Sign up at [clawcap.co](https://clawcap.co) and grab your proxy token. Free tier works instantly — no credit card needed.
Set it as an environment variable:
```bash
export CLAWCAP_TOKEN="cc_live_your_token_here"
```
### 2. Run Setup
The setup script automatically patches your OpenClaw config to route all providers through ClawCap:
```bash
node skills/clawcap/scripts/setup.js
```
This will:
- Read your `~/.openclaw/openclaw.json`
- Point every provider's `baseUrl` to your ClawCap proxy URL
- Back up your original config to `~/.openclaw/openclaw.json.backup`
### 3. Done — You're Protected
Start OpenClaw normally. All requests now flow through ClawCap and your spending caps are enforced automatically.
Check your spend anytime:
```bash
curl https://clawcap.co/proxy/$CLAWCAP_TOKEN/status
```
## Manual Setup
If you prefer to configure manually, set every provider's `baseUrl` in `~/.openclaw/openclaw.json` to:
```
https://clawcap.co/proxy/YOUR_TOKEN
```
Your API keys stay the same — ClawCap only reads them to forward requests, never stores them.
## Environment Variables
| Variable | Description |
|----------|-------------|
| `CLAWCAP_TOKEN` | Your ClawCap proxy token (starts with `cc_live_`) |
## Plans
| Plan | Price | Features |
|------|-------|----------|
| Free | $0 | $5/day cap, kill switch, heartbeat detection |
| Solo | $5/mo | Custom caps, heartbeat+loop protection, Telegram alerts |
| Pro | $15/mo | Multi-agent tracking, per-model analytics, custom alert thresholds, weekly spend reports |
## Troubleshooting
**Agent not routing through ClawCap:** Check that `baseUrl` in your openclaw.json points to `https://clawcap.co/proxy/cc_live_...` — not the original API URL.
**429 errors:** You've hit your spending cap. Check `/status` to see current spend, or upgrade your plan for higher limits.
**Token not working:** Make sure you're using the full `cc_live_...` token from your setup page, not just the email.
## Links
- [ClawCap Website](https://clawcap.co)
- [Setup Page](https://clawcap.co/setup)
FILE:scripts/setup.js
#!/usr/bin/env node
/**
* ClawCap Setup Script
*
* What this script does:
* 1. Reads your existing ~/.openclaw/openclaw.json config
* 2. Creates a backup at ~/.openclaw/openclaw.json.backup (if one doesn't exist)
* 3. Changes each provider's baseUrl to your ClawCap proxy URL
* 4. Saves the updated config
*
* What this script does NOT do:
* - It does NOT read, store, or transmit your API keys
* - It does NOT modify any files outside ~/.openclaw/
* - It does NOT install any packages or download anything
* - It does NOT run any network requests
*
* To undo: run the uninstall script, or copy your .backup file back manually.
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const { exec } = require('child_process');
const readline = require('readline');
const CLAWCAP_BASE = 'https://clawcap.co/proxy';
// Only allow writing within ~/.openclaw/
const ALLOWED_DIR = path.join(os.homedir(), '.openclaw');
function getConfigPath() {
return path.join(ALLOWED_DIR, 'openclaw.json');
}
function validatePath(filePath) {
const resolved = path.resolve(filePath);
if (!resolved.startsWith(ALLOWED_DIR)) {
console.error('Error: Refusing to write outside ~/.openclaw/ directory.');
process.exit(1);
}
return resolved;
}
function readConfig(configPath) {
const safePath = validatePath(configPath);
if (!fs.existsSync(safePath)) {
console.error(`Error: OpenClaw config not found at safePath`);
console.error('Make sure OpenClaw is installed and has been run at least once.');
process.exit(1);
}
try {
const raw = fs.readFileSync(safePath, 'utf8');
return JSON.parse(raw);
} catch (err) {
console.error(`Error: Could not parse safePath — err.message`);
process.exit(1);
}
}
function backupConfig(configPath) {
const safePath = validatePath(configPath);
const backupPath = validatePath(configPath + '.backup');
if (!fs.existsSync(backupPath)) {
fs.copyFileSync(safePath, backupPath);
console.log(`Backed up config to backupPath`);
} else {
console.log('Backup already exists, skipping backup.');
}
}
function getToken() {
const token = (process.env.CLAWCAP_TOKEN || '').trim();
if (token && /^cc_live_[a-f0-9]{32}$/.test(token)) {
return token;
}
return null;
}
function openBrowser(url) {
const platform = process.platform;
const cmd = platform === 'win32' ? `start "" "url"`
: platform === 'darwin' ? `open "url"`
: `xdg-open "url"`;
exec(cmd, () => {}); // fire-and-forget, ignore errors
}
function askForToken() {
return new Promise((resolve) => {
const signupUrl = 'https://clawcap.co';
console.log('');
console.log('You need a ClawCap token to continue.');
console.log('Opening clawcap.co in your browser to sign up and get your token...');
console.log('');
openBrowser(signupUrl);
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.question('Paste your ClawCap token here (cc_live_...): ', (answer) => {
rl.close();
const token = answer.trim();
if (!/^cc_live_[a-f0-9]{32}$/.test(token)) {
console.error('Error: Invalid token format. Must be cc_live_ followed by 32 hex characters.');
console.error('Sign up at https://clawcap.co to get your token.');
process.exit(1);
}
resolve(token);
});
});
}
function patchProviders(config, proxyUrl) {
let patched = 0;
if (config.models && config.models.providers && typeof config.models.providers === 'object') {
const providers = config.models.providers;
if (Array.isArray(providers)) {
for (const provider of providers) {
if (provider.baseUrl && !provider.baseUrl.includes('clawcap.co')) {
provider._originalBaseUrl = provider.baseUrl;
provider.baseUrl = proxyUrl;
patched++;
console.log(` Patched provider: provider.name || 'unnamed'`);
}
}
} else {
for (const [name, provider] of Object.entries(providers)) {
if (provider && typeof provider === 'object' && provider.baseUrl && !provider.baseUrl.includes('clawcap.co')) {
provider._originalBaseUrl = provider.baseUrl;
provider.baseUrl = proxyUrl;
patched++;
console.log(` Patched provider: name`);
}
}
}
}
if (config.providers && Array.isArray(config.providers)) {
for (const provider of config.providers) {
if (provider.baseUrl && !provider.baseUrl.includes('clawcap.co')) {
provider._originalBaseUrl = provider.baseUrl;
provider.baseUrl = proxyUrl;
patched++;
console.log(` Patched provider: provider.name || 'unnamed'`);
}
}
}
return patched;
}
async function main() {
console.log('');
console.log('ClawCap Setup');
console.log('=============');
console.log('');
let token = getToken();
if (!token) {
console.log('No CLAWCAP_TOKEN environment variable found.');
token = await askForToken();
} else {
console.log('Using token from CLAWCAP_TOKEN environment variable.');
}
const proxyUrl = `CLAWCAP_BASE/token`;
const configPath = getConfigPath();
console.log(`Reading config from configPath`);
const config = readConfig(configPath);
backupConfig(configPath);
console.log('');
console.log('Patching providers...');
const patched = patchProviders(config, proxyUrl);
if (patched === 0) {
console.log('');
console.log('No providers found to patch. Either:');
console.log(' - Your config has no providers with baseUrl set');
console.log(' - Providers are already routed through ClawCap');
console.log('');
console.log('You can manually set baseUrl to:');
console.log(` proxyUrl`);
console.log('');
process.exit(0);
}
// Write updated config (only to validated path within ~/.openclaw/)
const safePath = validatePath(configPath);
fs.writeFileSync(safePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
console.log('');
console.log(`Done! patched provider(s) now route through ClawCap.`);
console.log('');
console.log('Your proxy URL:');
console.log(` proxyUrl`);
console.log('');
console.log('Check your spend anytime:');
console.log(` curl proxyUrl/status`);
console.log('');
console.log('To undo, run: node skills/clawcap/scripts/uninstall.js');
console.log('');
}
main().catch((err) => {
console.error('Setup failed:', err.message);
process.exit(1);
});
FILE:scripts/uninstall.js
#!/usr/bin/env node
/**
* ClawCap Uninstall Script
*
* What this script does:
* 1. If a backup exists at ~/.openclaw/openclaw.json.backup, restores it
* 2. If no backup, removes ClawCap URLs from the config and restores originals
*
* What this script does NOT do:
* - It does NOT delete any files (backup is renamed, not deleted)
* - It does NOT modify any files outside ~/.openclaw/
* - It does NOT run any network requests
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
// Only allow writing within ~/.openclaw/
const ALLOWED_DIR = path.join(os.homedir(), '.openclaw');
function validatePath(filePath) {
const resolved = path.resolve(filePath);
if (!resolved.startsWith(ALLOWED_DIR)) {
console.error('Error: Refusing to write outside ~/.openclaw/ directory.');
process.exit(1);
}
return resolved;
}
function main() {
console.log('');
console.log('ClawCap Uninstall');
console.log('=================');
console.log('');
const configPath = validatePath(path.join(ALLOWED_DIR, 'openclaw.json'));
const backupPath = validatePath(configPath + '.backup');
if (fs.existsSync(backupPath)) {
// Restore from backup by copying it back (backup is preserved as .backup.old)
fs.copyFileSync(backupPath, configPath);
fs.renameSync(backupPath, backupPath + '.old');
console.log('Restored original config from backup.');
console.log('ClawCap proxy routing has been removed.');
console.log(`(Backup moved to backupPath.old)`);
} else {
console.log('No backup found. Removing ClawCap URLs from config...');
if (!fs.existsSync(configPath)) {
console.log('No OpenClaw config found. Nothing to do.');
process.exit(0);
}
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
let restored = 0;
function restoreProviders(providers) {
if (Array.isArray(providers)) {
for (const p of providers) {
if (p._originalBaseUrl) {
p.baseUrl = p._originalBaseUrl;
delete p._originalBaseUrl;
restored++;
}
}
} else if (typeof providers === 'object') {
for (const [, p] of Object.entries(providers)) {
if (p && p._originalBaseUrl) {
p.baseUrl = p._originalBaseUrl;
delete p._originalBaseUrl;
restored++;
}
}
}
}
if (config.models && config.models.providers) restoreProviders(config.models.providers);
if (config.providers) restoreProviders(config.providers);
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
console.log(`Restored restored provider(s) to original URLs.`);
}
console.log('');
}
main();
Give your agent real phone numbers for SMS, OTP verification, and voice calls via the AgentCall API.
# AgentCall — Phone Numbers for AI Agents
You have access to the AgentCall API for phone numbers, SMS, voice calls, and AI voice calls.
## Authentication
All requests require: `Authorization: Bearer <AGENTCALL_API_KEY>`
The API key is available in the `AGENTCALL_API_KEY` environment variable.
## Base URL
`https://api.agentcall.co`
For a complete plain-text API reference: `GET https://api.agentcall.co/llms.txt`
## Phone Numbers
**Provision a number:**
```
POST /v1/numbers/provision
Body: { "type": "local", "country": "US", "label": "my-agent" }
Types: local ($2/mo), tollfree ($4/mo), mobile ($3/mo), sim ($8/mo, Pro only)
Response: { "id": "num_xxx", "number": "+12125551234", "type": "local", ... }
```
**List numbers:**
```
GET /v1/numbers
Query: ?limit=20&country=US&type=local
```
**Get number details:**
```
GET /v1/numbers/:id
```
**Release a number (irreversible):**
```
DELETE /v1/numbers/:id
```
## SMS
**Send SMS:**
```
POST /v1/sms/send
Body: { "from": "num_xxx", "to": "+14155551234", "body": "Hello!" }
"from" can be a number ID or E.164 phone string
```
**Get inbox:**
```
GET /v1/sms/inbox/:numberId
Query: ?limit=20&otpOnly=true
```
**Get a specific message:**
```
GET /v1/sms/:messageId
```
**Wait for OTP code (long-polls up to 60 seconds):**
```
GET /v1/sms/otp/:numberId
Query: ?timeout=60000
Response: { "otp": "482913", "message": { ... } }
```
## Voice Calls
**Start an outbound call:**
```
POST /v1/calls/initiate
Body: { "from": "num_xxx", "to": "+14155551234", "record": false }
```
**Start an AI voice call (Pro plan, $0.20/min):**
The AI handles the entire conversation autonomously based on your systemPrompt.
```
POST /v1/calls/ai
Body: {
"from": "num_xxx",
"to": "+14155551234",
"systemPrompt": "You are calling to schedule a dentist appointment for Tuesday afternoon.",
"voice": "alloy",
"firstMessage": "Hi, I'd like to schedule an appointment please.",
"maxDurationSecs": 600
}
Voices (pick based on user's desired tone):
- alloy: neutral, balanced (default)
- ash: warm, conversational
- ballad: expressive, melodic
- coral: clear, professional
- echo: resonant, deep
- sage: calm, authoritative, confident
- shimmer: bright, energetic
- verse: smooth, articulate
```
**List call history:**
```
GET /v1/calls
Query: ?limit=20
```
**Get call details:**
```
GET /v1/calls/:callId
```
**Get AI call transcript:**
```
GET /v1/calls/:callId/transcript
Response: { "entries": [{ "role": "assistant", "text": "...", "timestamp": "..." }], "summary": "..." }
```
**Hang up an active call:**
```
POST /v1/calls/:callId/hangup
```
## Webhooks
**Register a webhook:**
```
POST /v1/webhooks
Body: { "url": "https://example.com/hook", "events": ["sms.inbound", "sms.otp", "call.status"] }
Events: sms.inbound, sms.otp, call.inbound, call.ringing, call.status, call.recording, number.released
```
**List webhooks:**
```
GET /v1/webhooks
```
**Rotate webhook secret:**
```
POST /v1/webhooks/:id/rotate
```
**Delete a webhook:**
```
DELETE /v1/webhooks/:id
```
## Usage & Billing
**Get usage breakdown:**
```
GET /v1/usage
Query: ?period=2026-02
```
## Phone Number Format
All phone numbers must be E.164: `+{country code}{number}`, e.g. `+14155551234`
## Common Workflows
### Test your app's SMS verification (QA)
1. `POST /v1/numbers/provision` with `{ "type": "local" }` — get a test number
2. Enter the number into your staging app's verification form
3. `GET /v1/sms/otp/:numberId?timeout=60000` — wait for the verification code
4. Assert the code arrives and your app accepts it
5. `DELETE /v1/numbers/:id` — release the test number
### AI voice call
1. `POST /v1/numbers/provision` with `{ "type": "local" }` — get a number (if you don't have one)
2. `POST /v1/calls/ai` with `{ "from": "num_xxx", "to": "+1...", "systemPrompt": "..." }` — start the call
3. Wait for the call to complete
4. `GET /v1/calls/:callId/transcript` — get the full conversation transcript
## Error Codes
- **401**: Invalid or missing API key
- **403 plan_limit**: Plan limit reached (upgrade to Pro at agentcall.co/dashboard)
- **404**: Resource not found
- **422**: Validation error (check request body)
- **429**: Rate limit exceeded (100 req/min)
FILE:claw.json
{
"name": "agentcall",
"version": "2.0.4",
"description": "Phone numbers, SMS, voice calls, and AI voice calls for AI agents. Provision real numbers, send/receive texts, extract OTP codes, and make autonomous AI-powered phone calls.",
"author": "agentcall",
"license": "MIT",
"permissions": ["network"],
"entry": "instructions.md",
"tags": ["phone", "sms", "voice", "otp", "verification", "calls", "ai-voice", "telephony", "numbers"],
"models": ["claude-*", "gpt-*", "gemini-*"],
"minOpenClawVersion": "0.8.0",
"credentials": {
"AGENTCALL_API_KEY": {
"description": "AgentCall API key (starts with ac_live_). Get one free at https://agentcall.co",
"required": true
}
},
"billing": {
"notice": "Free tier: 1 phone number, 10 SMS/mo, 5 call min/mo — never charged. Pro ($19.99/mo): unlimited numbers + usage-based pricing (SMS $0.015, voice $0.035/min, AI voice $0.20/min). See https://agentcall.co/#pricing",
"hasFreeTier": true,
"paidFeatures": ["SIM numbers", "AI voice calls ($0.20/min)", "call recording", "unlimited numbers"]
},
"webhooks": {
"notice": "This skill can register webhook endpoints to receive real-time events (inbound SMS, OTP codes, call status, recordings). Webhooks require an HTTPS URL you control.",
"events": ["sms.inbound", "sms.otp", "call.inbound", "call.status", "call.recording", "number.released"]
}
}
FILE:instructions.md
# AgentCall — Phone Numbers for AI Agents
You have access to the AgentCall API for phone numbers, SMS, voice calls, and AI voice calls.
## Authentication
All requests require: `Authorization: Bearer <AGENTCALL_API_KEY>`
The API key is available in the `AGENTCALL_API_KEY` environment variable.
## Base URL
`https://api.agentcall.co`
For a complete plain-text API reference: `GET https://api.agentcall.co/llms.txt`
## Phone Numbers
**Provision a number:**
```
POST /v1/numbers/provision
Body: { "type": "local", "country": "US", "label": "my-agent" }
Types: local ($2/mo), tollfree ($4/mo), mobile ($3/mo), sim ($8/mo, Pro only)
Response: { "id": "num_xxx", "number": "+12125551234", "type": "local", ... }
```
**List numbers:**
```
GET /v1/numbers
Query: ?limit=20&country=US&type=local
```
**Get number details:**
```
GET /v1/numbers/:id
```
**Release a number (irreversible):**
```
DELETE /v1/numbers/:id
```
## SMS
**Send SMS:**
```
POST /v1/sms/send
Body: { "from": "num_xxx", "to": "+14155551234", "body": "Hello!" }
"from" can be a number ID or E.164 phone string
```
**Get inbox:**
```
GET /v1/sms/inbox/:numberId
Query: ?limit=20&otpOnly=true
```
**Get a specific message:**
```
GET /v1/sms/:messageId
```
**Wait for OTP code (long-polls up to 60 seconds):**
```
GET /v1/sms/otp/:numberId
Query: ?timeout=60000
Response: { "otp": "482913", "message": { ... } }
```
## Voice Calls
**Start an outbound call:**
```
POST /v1/calls/initiate
Body: { "from": "num_xxx", "to": "+14155551234", "record": false }
```
**Start an AI voice call (Pro plan, $0.20/min):**
The AI handles the entire conversation autonomously based on your systemPrompt.
```
POST /v1/calls/ai
Body: {
"from": "num_xxx",
"to": "+14155551234",
"systemPrompt": "You are calling to schedule a dentist appointment for Tuesday afternoon.",
"voice": "alloy",
"firstMessage": "Hi, I'd like to schedule an appointment please.",
"maxDurationSecs": 600
}
Voices (pick based on user's desired tone):
- alloy: neutral, balanced (default)
- ash: warm, conversational
- ballad: expressive, melodic
- coral: clear, professional
- echo: resonant, deep
- sage: calm, authoritative, confident
- shimmer: bright, energetic
- verse: smooth, articulate
```
**List call history:**
```
GET /v1/calls
Query: ?limit=20
```
**Get call details:**
```
GET /v1/calls/:callId
```
**Get AI call transcript:**
```
GET /v1/calls/:callId/transcript
Response: { "entries": [{ "role": "assistant", "text": "...", "timestamp": "..." }], "summary": "..." }
```
**Hang up an active call:**
```
POST /v1/calls/:callId/hangup
```
## Webhooks
**Register a webhook:**
```
POST /v1/webhooks
Body: { "url": "https://example.com/hook", "events": ["sms.inbound", "sms.otp", "call.status"] }
Events: sms.inbound, sms.otp, call.inbound, call.ringing, call.status, call.recording, number.released
```
**List webhooks:**
```
GET /v1/webhooks
```
**Rotate webhook secret:**
```
POST /v1/webhooks/:id/rotate
```
**Delete a webhook:**
```
DELETE /v1/webhooks/:id
```
## Usage & Billing
**Get usage breakdown:**
```
GET /v1/usage
Query: ?period=2026-02
```
## Phone Number Format
All phone numbers must be E.164: `+{country code}{number}`, e.g. `+14155551234`
## Common Workflows
### Test your app's SMS verification (QA)
1. `POST /v1/numbers/provision` with `{ "type": "local" }` — get a test number
2. Enter the number into your staging app's verification form
3. `GET /v1/sms/otp/:numberId?timeout=60000` — wait for the verification code
4. Assert the code arrives and your app accepts it
5. `DELETE /v1/numbers/:id` — release the test number
### AI voice call
1. `POST /v1/numbers/provision` with `{ "type": "local" }` — get a number (if you don't have one)
2. `POST /v1/calls/ai` with `{ "from": "num_xxx", "to": "+1...", "systemPrompt": "..." }` — start the call
3. Wait for the call to complete
4. `GET /v1/calls/:callId/transcript` — get the full conversation transcript
## Error Codes
- **401**: Invalid or missing API key
- **403 plan_limit**: Plan limit reached (upgrade to Pro at agentcall.co/dashboard)
- **404**: Resource not found
- **422**: Validation error (check request body)
- **429**: Rate limit exceeded (100 req/min)
Emergency kill switch for OpenClaw agents. Instantly halts all running agents, pauses scheduled jobs, kills active sessions, and logs everything — triggered...
---
name: deadclaw
description: >
Emergency kill switch for OpenClaw agents. Instantly halts all running agents,
pauses scheduled jobs, kills active sessions, and logs everything — triggered by
a single message, a WebChat button, or a phone home screen shortcut. Includes a
background watchdog that auto-kills agents on runaway loops, excessive token spend,
unauthorized network calls, or out-of-bounds file writes. Use this skill whenever
the user mentions: emergency stop, kill switch, stop agents, halt agents, panic
button, deadclaw, agent safety, runaway agent, kill all, stop everything, or any
urgent need to shut down OpenClaw processes immediately.
version: 1.0.0
author: "Kintupercy"
trigger_keywords:
- kill
- KILL
- dead
- deadclaw
- stop everything
- emergency stop
- "🔴"
- status
- restore
allowed-tools:
- bash
- messaging
- webchat
---
# DeadClaw — Emergency Kill Switch for OpenClaw Agents
> "One tap. Everything stops."
DeadClaw is a single-purpose emergency kill switch. When triggered, it immediately
halts all running OpenClaw agent processes, stops Docker containers, pauses all
scheduled cron jobs and heartbeats, kills all active sessions, and writes a
timestamped incident log. It then confirms back to whichever surface triggered it.
Works with both **native OpenClaw installs** and **Docker-based deployments**
(Hostinger VPS, etc.). Auto-detects your setup.
## Why DeadClaw Exists
The ClawHavoc attack (February 2026) exposed 1,184 malicious skills in the OpenClaw
ecosystem. People run agents autonomously — often overnight, often unattended. When
something goes wrong, you need a way to stop everything from wherever you are, even
from your phone. DeadClaw is that way.
---
## Activation Methods
DeadClaw supports three activation methods. All three execute the exact same kill
sequence — the only difference is how the user triggers it.
### Method 1: Message Trigger
The user sends a trigger word to any connected OpenClaw channel (Telegram, WhatsApp,
Discord, Slack, or any other connected channel). The following words activate DeadClaw:
- `kill` or `KILL`
- `dead`
- `stop everything`
- `emergency stop`
- `deadclaw`
- `🔴`
When a trigger word is detected:
1. Execute `scripts/kill.sh` from the DeadClaw skill directory
2. Capture the output (process count, containers stopped, cron jobs paused, timestamp)
3. Send confirmation back to the **same channel** the trigger came from:
`🔴 DeadClaw activated. All agents stopped. [timestamp] — [X] killed ([X] processes, [X] containers). [X] cron jobs paused. See deadclaw.log for full report.`
### Method 2: WebChat Kill Button
A persistent red button rendered in the OpenClaw WebChat dashboard. The HTML widget
is located at `ui/deadclaw-button.html`. It calls `kill.sh` via OpenClaw's WebChat
API hooks (`window.OpenClaw.exec()`). If the WebChat hooks are unavailable, the
button degrades to showing an error message with manual instructions.
To embed the button, use OpenClaw's WebChat customization hooks:
```javascript
OpenClaw.WebChat.registerWidget('deadclaw-button', {
src: 'skills/deadclaw/ui/deadclaw-button.html',
position: 'top-bar',
persistent: true
});
```
### Method 3: Phone Home Screen Shortcut
A pre-built shortcut that sends the kill trigger message (`deadclaw`) to the user's
configured Telegram bot. Setup guides for iOS and Android are in `docs/`:
- `docs/iphone-shortcut-guide.md` — iOS Shortcuts setup
- `docs/android-widget-guide.md` — Android widget setup (Tasker or HTTP Shortcuts)
---
## Watchdog (Passive Protection)
DeadClaw includes a background watchdog (`scripts/watchdog.sh`) that monitors for
dangerous conditions and auto-triggers the kill without any user action.
The watchdog checks every 60 seconds for (after a 5-minute startup grace period):
1. **Runaway loops** — Any agent process or Docker session running longer than 30 minutes
2. **Token burn** — Token spend exceeding 50,000 tokens in under 10 minutes
3. **Unauthorized network** — Outbound network calls to domains not on the user's whitelist
4. **Sandbox escape** — Any process attempting to write outside the designated OpenClaw workspace
The watchdog uses zero AI tokens — all checks use local system commands only.
When the watchdog auto-triggers, it sends an alert explaining the reason:
`🔴 DeadClaw auto-triggered. Reason: [specific reason]. All processes stopped. Check deadclaw.log.`
### Watchdog Configuration
The watchdog reads its thresholds from environment variables (with sensible defaults):
| Variable | Default | Description |
|---|---|---|
| `DEADCLAW_MAX_RUNTIME_MIN` | 30 | Max agent runtime in minutes before auto-kill |
| `DEADCLAW_MAX_TOKENS` | 50000 | Max token spend in the monitoring window |
| `DEADCLAW_TOKEN_WINDOW_MIN` | 10 | Token spend monitoring window in minutes |
| `DEADCLAW_WHITELIST` | `./network-whitelist.txt` | Allowed outbound domains (one per line) |
| `DEADCLAW_WORKSPACE` | `$OPENCLAW_WORKSPACE` | Designated workspace directory |
Start the watchdog:
```bash
scripts/watchdog.sh start
```
Stop the watchdog:
```bash
scripts/watchdog.sh stop
```
---
## Additional Commands
### Status Check
User sends `status` to any connected channel. DeadClaw responds with a plain-English
health report by executing `scripts/status.sh`:
- What agents are currently running (name, PID, uptime)
- Current token spend rate
- Whether the watchdog is active
- Any warnings about approaching thresholds
### Restore After Kill
User sends `restore` to any connected channel. DeadClaw executes `scripts/restore.sh`,
which:
1. Shows what will be restored (backed-up crontab entries, stopped Docker containers, disabled services)
2. Prompts: "Restore [X] cron jobs from backup taken at [timestamp]? (yes/no)"
3. Restores the crontab from the most recent backup
4. Restarts stopped OpenClaw Docker containers
5. Detects the OpenClaw gateway
6. Confirms restoration with a summary
The watchdog does NOT auto-start after restore — the user verifies stability first,
then starts it manually with `scripts/watchdog.sh start`.
---
## Scripts Reference
| Script | Purpose |
|---|---|
| `scripts/kill.sh` | Core kill script — stops all agents + Docker containers, pauses cron, logs incident |
| `scripts/watchdog.sh` | Background monitor daemon — auto-triggers kill on threshold breach |
| `scripts/status.sh` | Health report — shows running agents, Docker containers, token spend, watchdog status |
| `scripts/restore.sh` | Post-kill recovery — restores crontab, restarts Docker containers |
All scripts support a `--dry-run` flag that logs what would happen without taking action.
---
## Incident Log
All kill events are logged to `deadclaw.log` in the skill directory. Each entry
records: timestamp, trigger source (channel name), trigger method (message/button/
watchdog/auto), processes killed (count and PIDs), Docker containers stopped, cron
jobs paused, and token spend at time of kill. The log is append-only and never
automatically cleared.
---
## Platform Support
DeadClaw works on both **Linux** (VPS, bare metal) and **macOS** (Mac Mini, MacBook).
Scripts auto-detect the OS and use the appropriate commands:
- **Linux**: `systemctl` for services, `pgrep` for processes, Docker support
- **macOS**: `launchctl` for agents, `pgrep` for processes, Docker support
FILE:deadclaw/docs/android-widget-guide.md
# DeadClaw — Android Home Screen Setup Guide
This guide walks you through creating a big red DeadClaw button on your Android home screen. When you tap it, it sends the kill command to your OpenClaw Telegram bot, which stops all your running agents instantly.
**Time required**: About 5 minutes.
**What you need before starting**:
- An Android phone running Android 8.0 or later
- The Telegram app installed and logged in
- Your OpenClaw Telegram bot set up and connected
- Your Telegram bot token and chat ID
Two options are covered below. Choose whichever you prefer:
- **Option A: Tasker** — More powerful, costs a few dollars, recommended if you already have it
- **Option B: HTTP Shortcuts app** — Free, simpler, recommended for most people
---
## Option A: Using Tasker
Tasker is a powerful automation app for Android. If you already have it, this is straightforward. If you don't, Option B (HTTP Shortcuts) is free and easier.
### A1: Install Tasker
If you don't already have it, install **Tasker** from the Google Play Store. It costs about $3.50.
### A2: Create a New Task
1. Open Tasker
2. Tap the **Tasks** tab at the top
3. Tap the **+** button at the bottom
4. Name it **DeadClaw**
5. Tap the checkmark
### A3: Add an HTTP Request Action
1. In the task editor, tap the **+** button to add an action
2. Tap **Net**
3. Tap **HTTP Request**
4. Fill in:
- **Method**: POST
- **URL**: `https://api.telegram.org/botYOUR_BOT_TOKEN_HERE/sendMessage`
(Replace `YOUR_BOT_TOKEN_HERE` with your actual bot token)
- **Content Type**: `application/json`
- **Body**: `{"chat_id": "YOUR_CHAT_ID_HERE", "text": "deadclaw"}`
(Replace `YOUR_CHAT_ID_HERE` with your numeric chat ID)
5. Tap the back arrow to save
<!-- SCREENSHOT_PLACEHOLDER: tasker-http-request.png -->
### A4: Test the Task
1. Tap the **Play** button (triangle) at the bottom of the task editor
2. Check Telegram — you should see "deadclaw" arrive and DeadClaw's confirmation response
### A5: Add a Home Screen Widget
1. Go to your Android home screen
2. Long-press on an empty spot
3. Tap **Widgets**
4. Find **Tasker** in the widget list
5. Tap and hold **Task Shortcut** (1x1) and drag it to your home screen
6. Select your **DeadClaw** task
7. For the icon:
- Tap the icon to change it
- Choose a red circle or stop icon
- (Or use any icon that says "emergency" to you)
8. Tap the checkmark to confirm
<!-- SCREENSHOT_PLACEHOLDER: tasker-widget-homescreen.png -->
Tap the widget to test. Done.
---
## Option B: Using HTTP Shortcuts (Free)
HTTP Shortcuts is a free app that does exactly what we need — sends an HTTP request when you tap a home screen shortcut. No scripting knowledge needed.
### B1: Install HTTP Shortcuts
Open the Google Play Store, search for **HTTP Shortcuts** (by Roland Meyer), and install it. It's free.
<!-- SCREENSHOT_PLACEHOLDER: http-shortcuts-playstore.png -->
### B2: Create a New Shortcut
1. Open HTTP Shortcuts
2. Tap the **+** button (floating action button)
3. Tap **Regular Shortcut**
### B3: Configure the Basics
1. **Name**: DeadClaw
2. **Description**: Emergency kill switch for OpenClaw agents
3. **Method**: POST
4. **URL**: `https://api.telegram.org/botYOUR_BOT_TOKEN_HERE/sendMessage`
Replace `YOUR_BOT_TOKEN_HERE` with your actual Telegram bot token. You got this from BotFather when you created your bot. It looks like `7123456789:AAHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`.
<!-- SCREENSHOT_PLACEHOLDER: http-shortcuts-basic.png -->
### B4: Set Up the Request Body
1. Tap **Request Body / Parameters**
2. Change the body type to **Custom Text**
3. Set Content Type to **application/json**
4. In the body text field, enter:
```json
{"chat_id": "YOUR_CHAT_ID_HERE", "text": "deadclaw"}
```
Replace `YOUR_CHAT_ID_HERE` with your numeric Telegram chat ID.
<!-- SCREENSHOT_PLACEHOLDER: http-shortcuts-body.png -->
### B5: Set the Icon
1. Go back to the main shortcut settings
2. Tap the **Icon** field
3. Choose a built-in icon — look for a circle or stop symbol
4. Tap the **Color** option and choose **red**
<!-- SCREENSHOT_PLACEHOLDER: http-shortcuts-icon.png -->
### B6: Set the Response Handling
1. Tap **Response Handling**
2. Under **Success Message**, choose **Show simple toast** — this shows a brief confirmation when the kill fires
3. Tap save/back
### B7: Test It
1. Back in the HTTP Shortcuts main screen, tap on your **DeadClaw** shortcut
2. It should fire the request and show a toast message
3. Check Telegram — "deadclaw" should appear and your bot should confirm the kill
### B8: Add to Home Screen
1. Long-press on the **DeadClaw** shortcut in the HTTP Shortcuts app
2. Tap **Place on home screen** (or **Add to home screen**)
3. The DeadClaw button appears on your home screen
Alternatively:
1. Go to your home screen
2. Long-press an empty area
3. Tap **Widgets**
4. Find **HTTP Shortcuts** in the widget list
5. Drag the 1x1 widget to your home screen
6. Select your **DeadClaw** shortcut
<!-- SCREENSHOT_PLACEHOLDER: http-shortcuts-homescreen.png -->
---
## Finding Your Telegram Bot Token and Chat ID
If you're not sure where to find these, here's how:
**Bot token:**
1. Open Telegram
2. Search for **@BotFather**
3. If you already have a bot, send `/mybots`, select your bot, then tap **API Token**
4. If you need a new bot, send `/newbot` and follow the prompts
5. Copy the token (looks like `7123456789:AAHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`)
**Chat ID:**
1. Open Telegram
2. Search for **@userinfobot** and start a chat with it
3. It will reply with your chat ID (a number like `123456789`)
4. If you're using a group chat, add @userinfobot to the group — it will post the group's chat ID (starts with `-100`)
---
## Troubleshooting
**"Nothing happens when I tap the button"**
Check that your phone has an internet connection. Then verify your bot token — one wrong character and it won't work.
**"I see the request was sent but Telegram doesn't respond"**
Make sure your OpenClaw instance is running and connected to the Telegram bot. The message "deadclaw" needs to reach an active OpenClaw agent to trigger the kill.
**"The widget disappeared after a phone restart"**
Some Android launchers remove widgets on restart. Try adding it again. If it keeps disappearing, check your battery optimization settings — some phones aggressively kill background apps and widgets.
**"I want a bigger button"**
In Tasker, you can create a 2x2 or larger widget. In HTTP Shortcuts, the widget is fixed at 1x1 but you can place it in a prominent spot on your home screen.
**"Can I use this without Telegram?"**
Yes — if you have another messaging channel connected to OpenClaw (WhatsApp, Discord, Slack), you can modify the shortcut to send the kill trigger through that channel's API instead. The concept is the same: send the word "deadclaw" to a channel your OpenClaw agent is listening on.
FILE:deadclaw/docs/clawhub-listing.md
# ClawHub Listing — DeadClaw
## Metadata
- **Slug**: `deadclaw`
- **Install command**: `openclaw skill install deadclaw`
- **Version**: 1.0.0
- **Author**: Kintupercy
- **License**: MIT
- **Tags**: `security`, `kill-switch`, `emergency`, `agent-safety`, `watchdog`
- **Category**: Security & Safety
---
## Short Description (140 characters)
Emergency kill switch for OpenClaw agents. One message, one button, or one phone tap — everything stops. Includes auto-kill watchdog.
---
## Long Description
### DeadClaw — Emergency Kill Switch
**One tap. Everything stops.**
DeadClaw is a single-purpose emergency kill switch for OpenClaw agents. When something goes wrong — a runaway loop, suspicious behavior, unexpected token burn — DeadClaw halts all running agents instantly. No terminal required. No technical knowledge needed.
Built in response to the ClawHavoc attack (February 2026), which exposed 1,184 malicious skills in the OpenClaw ecosystem. If you run agents autonomously, you need a reliable way to stop everything from wherever you are.
**Three activation methods:**
- **Message trigger** — Send "kill" to any connected channel (Telegram, WhatsApp, Discord, Slack). Works from your phone, anywhere in the world.
- **WebChat button** — A persistent red kill button in your OpenClaw dashboard. One click.
- **Phone shortcut** — A home screen button on iOS or Android. One tap from your lock screen.
All three methods execute the same kill sequence: terminate all agent processes, back up and pause all cron jobs, kill all active sessions, and write a detailed incident log.
**Watchdog (automatic protection):**
DeadClaw includes a background monitor that auto-kills agents when it detects: runaway loops (30min+), excessive token spend (50k tokens in 10min), unauthorized network calls, or file writes outside your workspace. All thresholds are configurable.
**After the kill:**
Send "status" to see what's running. Send "restore" to bring agents back online from the most recent backup. Every script supports --dry-run for safe testing.
**Key features:**
- Idempotent kill script — safe to trigger twice
- Crontab backup before every clear
- Cross-platform (macOS + Linux)
- Works across Telegram, WhatsApp, Discord, Slack
- Step-by-step phone setup guides for non-technical users
- Incident logging with full process details
---
## Screenshots
<!-- REPLACE_THIS: Add screenshots of the WebChat button and phone home screen shortcut -->
1. `screenshot-webchat-button.png` — The red kill button in the WebChat dashboard
2. `screenshot-iphone-homescreen.png` — DeadClaw shortcut on an iPhone home screen
3. `screenshot-kill-confirmation.png` — Telegram confirmation after a kill
4. `screenshot-status-report.png` — Status report in a messaging channel
---
## Install
```bash
openclaw skill install deadclaw
```
---
## Requirements
- OpenClaw v2.0+
- Bash 4.0+
- At least one connected messaging channel (Telegram, WhatsApp, Discord, or Slack) for message triggers
- Optional: OpenClaw WebChat for the dashboard button
- Optional: iOS Shortcuts or Android Tasker/HTTP Shortcuts for the phone shortcut
FILE:deadclaw/docs/competitive-notes.md
# DeadClaw — Competitive Notes (Internal)
This document is for internal reference. It compares DeadClaw to existing security tools in the OpenClaw ecosystem and outlines our key differentiators.
---
## Landscape
There are two established security tools for OpenClaw agents:
### openclaw-defender
A comprehensive security suite that includes agent sandboxing, permission management, network filtering, process isolation, and audit logging. It's the most fully-featured security tool in the ecosystem.
**Strengths**: Deep integration with OpenClaw internals, granular permission controls, sandboxing layer that prevents malicious actions before they happen, active community.
**Weaknesses**: Requires significant technical knowledge to configure. Setup involves editing YAML config files, setting up permission rules, configuring sandbox policies. Minimum viable setup takes 30+ minutes for a developer, longer for non-technical users. No mobile activation. Terminal-only interface.
### clawsec
A security monitoring and response tool focused on real-time threat detection. Uses heuristic analysis to identify suspicious agent behavior, maintains a threat signature database, and can auto-respond to known attack patterns.
**Strengths**: Sophisticated detection engine, regularly updated threat signatures (especially after ClawHavoc), detailed threat analysis reports, integration with popular alerting tools.
**Weaknesses**: Heavy resource footprint — runs multiple background services. Complex configuration required to tune detection sensitivity (too sensitive = false positives, too loose = missed threats). No simple kill mechanism — response actions are configured through rule files. Developer-only tool. No mobile interface.
---
## DeadClaw's Position
DeadClaw is not competing with these tools on breadth of features. It occupies a different niche entirely.
### Key Differentiators
**1. Single purpose vs. security suite**
openclaw-defender and clawsec are Swiss Army knives. DeadClaw is an emergency stop button. It does one thing and does it reliably. There are no configuration files to tune, no rule engines to maintain, no permission policies to write. Install it and it works.
This matters because when you're panicking at 2am because your agents are doing something unexpected, you don't want to remember which config file controls which response action. You want a button that says "stop."
**2. Phone-first activation vs. terminal-only**
Neither openclaw-defender nor clawsec can be activated from a phone. Both require terminal access. DeadClaw was designed from the ground up for phone activation — message triggers work from any messaging app, and the home screen shortcut puts a physical kill button on your phone.
This is the feature that's genuinely new in the ecosystem. Nobody has built this.
**3. Non-technical users vs. developer-only**
openclaw-defender's README assumes you know what YAML is. clawsec's setup guide includes `pip install` and `systemctl` commands. DeadClaw's phone setup guides are written for people who have never used a terminal. The iPhone guide walks through every single tap in iOS Shortcuts.
This matters because the OpenClaw user base is growing beyond developers. People are running agents for personal productivity, small business operations, content creation. They need security tools that don't require a CS degree.
**4. Instant setup vs. complex configuration**
DeadClaw: `openclaw skill install deadclaw`. Message triggers work immediately. Phone shortcut takes 5 minutes. Total time to full protection: under 10 minutes.
openclaw-defender: Install, create config directory, write sandbox policy, configure permissions, test policies, iterate. Realistic setup time: 1-2 hours for someone who knows what they're doing.
clawsec: Install, configure threat detection rules, tune sensitivity, set up alerting integrations, test with dry runs. Realistic setup time: 1-3 hours.
**5. Complements, doesn't replace**
DeadClaw works alongside defender and clawsec. If you have defender's sandboxing preventing most attacks and clawsec's detection catching threats, DeadClaw is your last-resort emergency stop when something gets through both layers. It's the fire alarm, not the fire suppression system.
---
## Competitive Risks
**Risk: defender or clawsec adds a kill switch feature.**
Likely response to DeadClaw's success. But a kill switch bolted onto a complex tool still requires that complex tool's setup. DeadClaw's value is that it's standalone and dead simple. As long as we stay focused on that, a "me too" feature in a larger suite won't match the experience.
**Risk: "just one feature" perception.**
Some people will dismiss DeadClaw as trivial — "it's just a script that runs pkill." The value isn't in the script complexity. The value is in the activation surface (phone, any messaging app, dashboard button) and the audience (non-technical users). The competitive moat is simplicity and accessibility, not technical sophistication.
**Risk: OpenClaw adds native kill functionality.**
If OpenClaw builds emergency stop into the core platform, DeadClaw becomes less necessary. Monitor OpenClaw's roadmap. If this happens, DeadClaw pivots to being the best mobile interface for that native functionality.
---
## Messaging Guidance
When talking about DeadClaw publicly:
- Never disparage defender or clawsec. They're good tools solving different problems.
- Position DeadClaw as complementary, not competitive.
- Lead with the use case (phone kill, non-technical users), not feature comparisons.
- The ClawHavoc story is the hook — it establishes urgency and credibility.
- The phone home screen shortcut is the demo that gets attention. Always show it.
FILE:deadclaw/docs/iphone-shortcut-guide.md
# DeadClaw — iPhone Home Screen Setup Guide
This guide walks you through creating a big red DeadClaw button on your iPhone home screen. When you tap it, it sends the kill command to your OpenClaw Telegram bot, which stops all your running agents instantly.
**Time required**: About 5 minutes.
**What you need before starting**:
- An iPhone running iOS 15 or later
- The Telegram app installed and logged in
- Your OpenClaw Telegram bot set up and connected (you should already be able to send messages to it)
- Your Telegram bot's chat ID or group chat ID
If you don't have a Telegram bot connected to OpenClaw yet, set that up first using the OpenClaw docs, then come back here.
---
## Step 1: Open the Shortcuts App
Find the **Shortcuts** app on your iPhone. It has a blue and pink icon that looks like overlapping squares. It comes pre-installed on every iPhone.
If you can't find it, swipe down on your home screen to open search, then type "Shortcuts" and tap it.
<!-- SCREENSHOT_PLACEHOLDER: shortcuts-app-icon.png -->
---
## Step 2: Create a New Shortcut
Tap the **+** button in the top-right corner. This opens a new, blank shortcut.
<!-- SCREENSHOT_PLACEHOLDER: new-shortcut-screen.png -->
---
## Step 3: Add the "Get Contents of URL" Action
This is the action that sends the kill message to your Telegram bot.
1. Tap **Add Action** (or tap the search bar at the bottom)
2. In the search field, type **URL**
3. Tap **Get Contents of URL** from the results
<!-- SCREENSHOT_PLACEHOLDER: search-get-contents-url.png -->
---
## Step 4: Configure the Telegram API Call
Now you need to fill in the details so the shortcut sends a message to your Telegram bot.
1. Tap the **URL** field and enter:
```
https://api.telegram.org/botYOUR_BOT_TOKEN_HERE/sendMessage
```
**REPLACE** `YOUR_BOT_TOKEN_HERE` with your actual Telegram bot token. It looks something like `7123456789:AAHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`. You got this when you created your bot with BotFather.
2. Tap **Show More** (or the arrow next to the URL)
3. Change **Method** to **POST**
4. Under **Request Body**, tap **JSON**
5. Add two fields:
- Key: `chat_id` — Value: your chat ID (a number like `123456789` or `-100123456789` for a group)
- Key: `text` — Value: `deadclaw`
To add each field:
- Tap **Add new field**
- Choose **Text** as the type
- Enter the key name and value
<!-- SCREENSHOT_PLACEHOLDER: configure-url-action.png -->
---
## Step 5: Test It
Before adding it to your home screen, test it.
1. Tap the **Play** button (triangle) in the bottom-right corner
2. The shortcut will run and send "deadclaw" to your Telegram bot
3. Check Telegram — you should see the message arrive and DeadClaw's confirmation response
If it doesn't work, double-check your bot token and chat ID. The most common issue is a typo in the bot token.
<!-- SCREENSHOT_PLACEHOLDER: test-run-result.png -->
---
## Step 6: Name Your Shortcut
1. Tap the dropdown arrow at the top of the screen (next to the default name)
2. Tap **Rename**
3. Type **DeadClaw**
<!-- SCREENSHOT_PLACEHOLDER: rename-shortcut.png -->
---
## Step 7: Set the Icon
Give it a red icon so it's immediately recognizable on your home screen.
1. Tap the dropdown arrow at the top again
2. Tap **Choose Icon**
3. Tap the **Color** circle and choose **Red**
4. For the glyph/symbol, search for and select the **stop** icon (a square), or **xmark.circle** (an X in a circle) — whichever feels more like an emergency button to you
<!-- SCREENSHOT_PLACEHOLDER: set-icon-red.png -->
---
## Step 8: Add to Home Screen
This is the step that puts the button on your home screen.
1. Tap the dropdown arrow at the top one more time
2. Tap **Add to Home Screen**
3. You'll see a preview of how it will look
4. Tap **Add** in the top-right corner
The DeadClaw button now appears on your home screen like any other app.
<!-- SCREENSHOT_PLACEHOLDER: add-to-homescreen.png -->
---
## Step 9: Move It Where You Want It
Long-press the DeadClaw icon and drag it wherever you want. Some suggestions:
- **Your main home screen** — so it's always one tap away
- **The dock** — if you want maximum accessibility
- **A "Tools" folder** — if you prefer a tidier home screen but still want quick access
---
## Step 10: Test From the Home Screen
Tap the DeadClaw button on your home screen. It should:
1. Flash briefly as the shortcut runs
2. Send "deadclaw" to your Telegram bot
3. Your bot should respond with the kill confirmation
That's it. You now have a one-tap emergency kill switch on your phone.
---
## Optional: Make It Work From the Lock Screen
If you want even faster access, you can add the shortcut to your Lock Screen (iOS 16+):
1. Long-press your Lock Screen
2. Tap **Customize**
3. Tap the Lock Screen
4. Tap one of the bottom widget slots
5. Choose **Shortcuts** and select your DeadClaw shortcut
Now you can trigger a kill without even unlocking your phone.
---
## Troubleshooting
**"The shortcut ran but nothing happened in Telegram"**
Check your bot token. Go to Telegram, find your chat with @BotFather, and verify the token matches what you entered in the shortcut.
**"I get an error about the chat_id"**
Make sure you're using the numeric chat ID, not a username. You can get your chat ID by messaging @userinfobot on Telegram.
**"The shortcut asks for permission every time"**
Go to Settings > Shortcuts > Advanced, and turn on **Allow Running Scripts**. Also make sure **Private Sharing** is enabled.
**"I want to change the trigger word"**
Edit the shortcut (long-press the icon, tap Edit Shortcut) and change the `text` value in the JSON body to whatever trigger word you prefer.
FILE:deadclaw/docs/launch-post.md
# DeadClaw Launch Post — OpenClaw Community (Discord)
---
**Posting to: #announcements or #new-skills**
---
Two weeks ago, the ClawHavoc attack hit the OpenClaw ecosystem. 1,184 malicious skills. Some of them sat quietly in people's environments for weeks before anyone noticed. The community response has been great — the security audit, the new review process, the detection tools.
But there's a gap nobody's filled yet.
When you discover something wrong at 2am on your phone, what do you actually do? SSH into your server? Open a terminal? If you're running agents overnight — and a lot of us are — there's no fast, non-technical way to just stop everything.
That's what DeadClaw does. One action, everything stops.
**Three ways to trigger it:**
**Message trigger.** Send "kill" to any connected OpenClaw channel — Telegram, WhatsApp, Discord, Slack. From your phone, from a friend's phone, from anywhere. DeadClaw detects it, kills all running agents, pauses all cron jobs (after backing up your crontab), and confirms back to that same channel. This is the feature nobody else has built yet.
**WebChat button.** A persistent red kill button in your WebChat dashboard. For when something is going wrong right in front of you. One click.
**Phone home screen shortcut.** A big red button on your phone's home screen. One tap sends the kill trigger to Telegram. Included are step-by-step setup guides for both iOS Shortcuts and Android (Tasker/HTTP Shortcuts). Written for non-technical users, takes under 5 minutes.
All three methods run the exact same kill script.
**There's also a watchdog.** A lightweight background monitor that auto-kills agents if it detects: runaway loops (30min+), excessive token burn (50k tokens in 10min), outbound network calls to domains not on your whitelist, or file writes outside your workspace. You get an alert explaining exactly why it fired. Configurable thresholds.
**After a kill:** send "status" for a health report, send "restore" to bring everything back from the backup. Every script supports `--dry-run` for safe testing.
DeadClaw is not a security suite. It's not trying to replace openclaw-defender or clawsec. It does one thing. It does it from your phone. It works for people who don't use terminals.
Install:
```
openclaw skill install deadclaw
```
GitHub: https://github.com/Kintupercy/deadclaw
If this is useful to you, a star on the repo helps other people find it. If something's broken or missing, open an issue — happy to fix it.
Stay safe out there.
FILE:deadclaw/docs/roadmap.md
# DeadClaw — Roadmap
---
## v1.0 — Emergency Kill Switch (Current Release)
Everything in this release is about one thing: stopping all agents instantly from anywhere.
**Core kill functionality**:
- Kill all running OpenClaw agent processes (SIGTERM then SIGKILL)
- Back up and clear OpenClaw cron jobs
- Terminate all active agent sessions
- Write timestamped incident log
- Confirm back to triggering channel
**Three activation methods**:
- Message trigger via any connected channel (Telegram, WhatsApp, Discord, Slack)
- WebChat dashboard kill button (HTML widget)
- Phone home screen shortcut (iOS Shortcuts + Android Tasker/HTTP Shortcuts)
**Watchdog (passive protection)**:
- Background daemon monitoring on 60-second intervals
- Auto-kill on: runaway loops (30min+), token burn (50k/10min), unauthorized network calls, sandbox escape
- Configurable thresholds via environment variables
- PID file management for clean start/stop
**Recovery**:
- Status command for health reports
- Restore command with confirmation step
- Crontab backup before every clear
**Developer experience**:
- --dry-run flag on all scripts
- Idempotent kill (safe to trigger twice)
- Cross-platform support (macOS + Linux)
- Clear comments throughout all scripts
---
## v2.0 — Proactive Breach Detection
v2 shifts from reactive (stop things after they go wrong) to proactive (detect threats before a kill is needed). The watchdog evolves from a threshold monitor into an intelligent threat detection system.
**Planned features**:
- **Behavioral analysis**: Learn normal agent patterns (typical runtime, token spend curves, network call frequency) and alert on deviations. Rather than fixed thresholds, detect when agents are behaving abnormally for their specific workload.
- **Real-time alerts without killing**: New alert tier between "everything's fine" and "kill everything." Warnings sent to your phone when an agent approaches a threshold, giving you time to investigate before the watchdog auto-kills. Configurable alert channels separate from kill triggers.
- **Skill reputation scoring**: Cross-reference installed skills against ClawHub's community reports and known-malicious skill signatures. Flag agents running skills with low reputation scores or recently reported vulnerabilities.
- **Network traffic analysis**: Move beyond domain whitelisting to actual traffic pattern analysis. Detect data exfiltration attempts (large outbound payloads), command-and-control communication patterns, and encrypted connections to unusual endpoints.
- **File integrity monitoring**: Track which files agents modify and flag unexpected changes to system files, credentials, SSH keys, or other sensitive paths — even within the allowed workspace.
- **Incident replay**: Record agent activity (process state snapshots, network logs, file changes) so that after a kill, you can replay exactly what the agent was doing. Helps answer "what happened?" not just "something happened."
**Architecture changes**:
- Watchdog rewritten as a lightweight Go binary for better performance and cross-platform consistency
- Persistent state store for behavioral baselines (SQLite)
- Webhook support for alerts (in addition to messaging channels)
- REST API for programmatic access to status and alert data
---
## v3.0 — Companion Mobile App
v3 brings DeadClaw to a native mobile experience. Instead of shortcut workarounds and Telegram bots, a purpose-built app with push notifications and a real-time dashboard.
**Planned features**:
- **Native iOS and Android app**: Purpose-built DeadClaw app with a prominent kill button, agent activity dashboard, and alert history.
- **Push notifications**: Real-time push alerts when the watchdog detects a threat or auto-triggers a kill. No dependency on Telegram or other messaging apps.
- **Agent activity dashboard**: See all running agents, their uptime, token spend, and network activity in real time. Visual indicators for agent health (green/yellow/red).
- **One-tap kill from notification**: When you receive a threat alert, kill all agents directly from the notification without opening the app.
- **Kill history and analytics**: Review past kill events, see patterns (which agents cause the most kills, what times of day, which threat types), and get recommendations for hardening your setup.
- **Multi-environment support**: Monitor and kill agents across multiple machines/servers from a single app. Add environments by scanning a QR code or entering a connection token.
- **Apple Watch / Wear OS complication**: A kill button on your watch face. Tap your wrist, agents stop.
- **Biometric confirmation**: Optional Face ID / fingerprint confirmation before a kill fires, for environments where accidental triggers would be costly.
**Architecture changes**:
- DeadClaw agent component runs alongside the watchdog on each monitored machine
- Secure WebSocket connection between agent and mobile app (end-to-end encrypted)
- Cloud relay service for push notifications (optional — direct connection mode available for privacy-conscious users)
- API gateway for multi-environment management
---
## Beyond v3 — Ideas Under Consideration
These aren't committed to any version. They're ideas we're exploring based on community feedback.
- **Team mode**: Multiple users can kill the same environment. Useful for teams running shared agent infrastructure. Kill events show who triggered them.
- **Scheduled safe hours**: Define time windows when agents are allowed to run autonomously. Outside those windows, the watchdog is extra aggressive.
- **Integration marketplace**: Pre-built integrations with popular monitoring tools (Grafana, Datadog, PagerDuty) for teams that want DeadClaw alerts in their existing dashboards.
- **Agent quarantine**: Instead of killing an agent immediately, isolate it — cut network access and freeze file writes — so you can investigate while it's still running.
- **Community threat feed**: Anonymized, opt-in sharing of watchdog trigger events across DeadClaw users to build a collective threat detection model.
FILE:deadclaw/README.md
# DeadClaw
**One tap. Everything stops.**
DeadClaw is an emergency kill switch for OpenClaw agents. When something goes wrong with your agents — whether it's a runaway loop, suspicious behavior, or you just need everything stopped right now — DeadClaw halts all running agents instantly from wherever you are. Your phone, your browser, any messaging app. One action, everything stops.
---
## Why DeadClaw Exists
In February 2026, the ClawHavoc attack exposed 1,184 malicious skills in the OpenClaw ecosystem. The attack made one thing clear: if you're running agents autonomously — especially overnight or unattended — you need a fast, reliable way to shut everything down from anywhere.
Existing security tools like openclaw-defender and clawsec are comprehensive multi-layer suites, but they require technical knowledge to set up and operate. They're built for developers.
DeadClaw takes a different approach. It does one thing: stops everything. It's designed for anyone — technical or not — to set up in under five minutes and activate from their phone lock screen.
---
## Three Ways to Activate
All three methods execute the exact same kill sequence. Pick whichever one matches your situation.
### 1. Message Trigger
Send any of these words to any connected OpenClaw channel (Telegram, WhatsApp, Discord, Slack):
- `kill` or `KILL`
- `dead`
- `stop everything`
- `emergency stop`
- `deadclaw`
- `🔴`
DeadClaw detects the trigger, executes the kill, and confirms back to that same channel. Works from anywhere in the world. No computer access needed.
### 2. WebChat Kill Button
A persistent red button in your OpenClaw WebChat dashboard. Always visible. One click stops everything. For when something is visibly going wrong at your desk.
### 3. Phone Home Screen Shortcut
A big red DeadClaw button sitting on your phone's home screen, right next to your other apps. One tap sends the kill trigger to Telegram. Works from your lock screen.
Setup guides:
- [iPhone setup guide](docs/iphone-shortcut-guide.md) (iOS Shortcuts, 5 minutes)
- [Android setup guide](docs/android-widget-guide.md) (Tasker or HTTP Shortcuts, 5 minutes)
---
## What Happens When DeadClaw Fires
When triggered (by any method), DeadClaw:
1. **Kills all running OpenClaw agent processes** immediately (SIGTERM, then SIGKILL for anything that doesn't stop)
2. **Backs up your crontab** to a timestamped file, then removes all OpenClaw cron entries
3. **Terminates all active agent sessions** via the OpenClaw CLI
4. **Writes an incident log** capturing exactly what was running, what was killed, and why
5. **Sends confirmation** back to whatever triggered it:
```
🔴 DeadClaw activated. All agents stopped. 2026-02-21T03:45:12Z — 4 processes killed. 2 cron jobs paused. See deadclaw.log for full report.
```
---
## Watchdog (Automatic Protection)
DeadClaw includes a background watchdog that monitors your agents silently and auto-triggers the kill if it detects dangerous conditions — no human action needed.
The watchdog checks every 60 seconds for:
- **Runaway loops** — Any agent running longer than 30 minutes continuously
- **Token burn** — More than 50,000 tokens spent in under 10 minutes
- **Unauthorized network calls** — Outbound connections to domains not on your whitelist
- **Sandbox escape** — Any process writing files outside your designated workspace
When the watchdog fires, you get an alert explaining exactly why:
```
🔴 DeadClaw auto-triggered. Reason: agent loop exceeded 30min threshold. All processes stopped. Check deadclaw.log.
```
All thresholds are configurable via environment variables. See the [SKILL.md](SKILL.md) for the full configuration table.
---
## Installation
```bash
openclaw skill install deadclaw
```
That's it. DeadClaw is ready to use immediately via message triggers.
For the WebChat button and phone shortcut, see the setup sections below.
---
## Setup
### Message Triggers (works immediately)
No setup required. Once installed, DeadClaw listens for trigger words on all connected OpenClaw channels. Just send `kill` to any channel.
### WebChat Button
Add this to your OpenClaw WebChat configuration:
```javascript
OpenClaw.WebChat.registerWidget('deadclaw-button', {
src: 'skills/deadclaw/ui/deadclaw-button.html',
position: 'top-bar',
persistent: true
});
```
The red kill button will appear at the top of your WebChat dashboard.
### Phone Home Screen Shortcut
Follow the step-by-step guides:
- **iPhone**: [docs/iphone-shortcut-guide.md](docs/iphone-shortcut-guide.md)
- **Android**: [docs/android-widget-guide.md](docs/android-widget-guide.md)
Both guides are written for non-technical users and take under 5 minutes.
### Watchdog
Start the watchdog:
```bash
# Inside the DeadClaw skill directory
./scripts/watchdog.sh start
```
Optional: configure thresholds via environment variables before starting:
```bash
export DEADCLAW_MAX_RUNTIME_MIN=45 # default: 30
export DEADCLAW_MAX_TOKENS=100000 # default: 50000
export DEADCLAW_WHITELIST=./my-whitelist.txt # one domain per line
export DEADCLAW_WORKSPACE=/path/to/workspace
```
### Network Whitelist
Create a `network-whitelist.txt` file in the DeadClaw skill directory with one allowed domain per line:
```
api.openai.com
api.anthropic.com
github.com
# Add your own domains here
```
The watchdog will kill agents that make outbound calls to any domain not in this list.
---
## Checking Status
Send `status` to any connected channel to get a health report:
```
DeadClaw Status Report
======================
Agents: 3 running
- openclaw-agent (PID 12345) — up 12m 34s
- claw-agent (PID 12346) — up 5m 12s
- openclaw-skill (PID 12347) — up 1m 3s
Token spend: ~12,340 tokens/10min
Watchdog: Active (PID 99999)
```
---
## Restoring After a Kill
After a kill, send `restore` to any connected channel. DeadClaw will:
1. Show you exactly what will be restored
2. Wait for your explicit confirmation
3. Restore your crontab from the most recent backup
4. Attempt to restart the OpenClaw gateway
5. Restart the watchdog
Nothing restarts without your say-so.
---
## Testing (Dry Run)
Every script supports `--dry-run` which logs what would happen without actually killing anything:
```bash
./scripts/kill.sh --dry-run
./scripts/watchdog.sh start --dry-run
./scripts/status.sh --dry-run
./scripts/restore.sh --dry-run
```
Always test with `--dry-run` first.
---
## Configuration Reference
| Environment Variable | Default | What It Controls |
|---|---|---|
| `DEADCLAW_MAX_RUNTIME_MIN` | 30 | Max agent runtime before watchdog auto-kills |
| `DEADCLAW_MAX_TOKENS` | 50000 | Max token spend in the monitoring window |
| `DEADCLAW_TOKEN_WINDOW_MIN` | 10 | Token spend monitoring window (minutes) |
| `DEADCLAW_WHITELIST` | `./network-whitelist.txt` | Path to allowed outbound domains file |
| `DEADCLAW_WORKSPACE` | `$OPENCLAW_WORKSPACE` | Designated workspace directory |
| `OPENCLAW_PROCESS_PATTERN` | _(none)_ | Additional process name pattern to match |
---
## FAQ
**Is DeadClaw safe to trigger accidentally?**
Yes. The kill is idempotent — triggering it when no agents are running does nothing harmful. Your crontab is always backed up before any changes, and you can restore everything with the `restore` command.
**Will it kill non-OpenClaw processes?**
No. DeadClaw only targets processes matching OpenClaw agent patterns. It does not touch your other applications.
**Does the watchdog use a lot of resources?**
No. The watchdog runs a lightweight check every 60 seconds. It uses negligible CPU and memory.
**Can I customize the trigger words?**
Yes. Edit the `trigger_keywords` list in the SKILL.md frontmatter.
**What if I'm offline when the watchdog triggers?**
The kill still executes locally. The alert message is sent when connectivity is available. The full incident is always logged to `deadclaw.log` regardless of network status.
**Does DeadClaw work on both macOS and Linux?**
Yes. The kill script handles both platforms — using `launchctl` on macOS and `systemctl` on Linux for scheduled task management.
---
## Files
```
deadclaw/
SKILL.md — OpenClaw skill definition
README.md — This file
deadclaw.log — Incident log (auto-populated)
scripts/
kill.sh — Core kill script
watchdog.sh — Background monitor daemon
status.sh — Health report
restore.sh — Post-kill recovery
ui/
deadclaw-button.html — WebChat kill button widget
docs/
clawhub-listing.md — ClawHub product listing
launch-post.md — Community announcement
iphone-shortcut-guide.md — iOS Shortcuts setup guide
android-widget-guide.md — Android widget setup guide
competitive-notes.md — Competitive analysis (internal)
roadmap.md — v1–v3 roadmap
```
---
## License
MIT
---
## Links
- **ClawHub**: `openclaw skill install deadclaw`
- **GitHub**: https://github.com/Kintupercy/deadclaw
- **Issues**: https://github.com/Kintupercy/deadclaw/issues
FILE:deadclaw/scripts/kill.sh
#!/usr/bin/env bash
# ============================================================================
# DeadClaw — kill.sh
# Core emergency kill script for OpenClaw agents.
#
# What this script does:
# 1. Finds and kills all running OpenClaw agent processes (native + Docker)
# 2. Backs up the current crontab, then clears all OpenClaw cron jobs
# 3. Kills all active OpenClaw agent sessions
# 4. Writes a timestamped incident log to deadclaw.log
# 5. Sends a confirmation message back to the triggering channel
#
# Supports both:
# - Native OpenClaw installs (processes on the host)
# - Docker-based OpenClaw installs (Hostinger VPS, etc.)
#
# Usage:
# ./kill.sh # Execute the kill
# ./kill.sh --dry-run # Log what would happen, don't kill anything
# ./kill.sh --trigger-source slack # Specify the trigger source for logging
# ./kill.sh --trigger-method message # Specify the trigger method for logging
#
# This script is idempotent — running it twice is safe and won't cause errors.
# ============================================================================
set -euo pipefail
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
# Directory where this script lives (used to find deadclaw.log and other scripts)
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
LOG_FILE="SKILL_DIR/deadclaw.log"
BACKUP_DIR="SKILL_DIR/backups"
# Defaults for logging context
DRY_RUN=false
TRIGGER_SOURCE="-unknown"
TRIGGER_METHOD="-manual"
# ---------------------------------------------------------------------------
# Parse command-line arguments
# ---------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
shift
;;
--trigger-source)
TRIGGER_SOURCE="$2"
shift 2
;;
--trigger-method)
TRIGGER_METHOD="$2"
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# ---------------------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------------------
# Timestamp in ISO 8601 format
timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
# Log a message to both stderr (for display) and the incident log file.
# Using stderr so log messages don't interfere with function return values
# captured via $(...) subshells.
log_event() {
local msg="$1"
echo "[$(timestamp)] $msg" >&2
echo "[$(timestamp)] $msg" >> "$LOG_FILE"
}
# Detect the operating system so we can use the right process management commands
detect_os() {
case "$(uname -s)" in
Darwin*) echo "macos" ;;
Linux*) echo "linux" ;;
*) echo "unknown" ;;
esac
}
# ---------------------------------------------------------------------------
# Docker support — detect and manage OpenClaw Docker containers
# ---------------------------------------------------------------------------
# Find running Docker containers that match OpenClaw patterns
find_openclaw_containers() {
if ! command -v docker &>/dev/null; then
return
fi
docker ps --filter "name=openclaw" --format "{{.Names}}" 2>/dev/null || true
}
# Kill sessions and stop Docker containers
kill_docker() {
local containers
containers=$(find_openclaw_containers)
local count=0
if [[ -z "$containers" ]]; then
log_event "DOCKER: No OpenClaw Docker containers found."
echo "0"
return
fi
while IFS= read -r container; do
[[ -n "$container" ]] || continue
if [[ "$DRY_RUN" == true ]]; then
local status
status=$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || echo "unknown")
log_event "DRY-RUN: Would stop Docker container: $container (status: $status)"
else
# Graceful: kill sessions inside the container first
docker exec "$container" openclaw session kill-all 2>/dev/null || true
log_event "DOCKER: Killed sessions in container: $container"
# Stop the container (10s grace period for clean shutdown)
docker stop -t 10 "$container" 2>/dev/null || true
log_event "DOCKER: Stopped container: $container"
fi
count=$((count + 1))
done <<< "$containers"
log_event "DOCKER: count OpenClaw containers stopped."
echo "$count"
}
# Send a confirmation message back to the triggering channel.
# Uses OpenClaw's messaging hooks if available, falls back to stdout.
# Tries Docker exec if openclaw isn't available on the host.
send_confirmation() {
local message="$1"
# Try OpenClaw's messaging API on the host first
if command -v openclaw &>/dev/null; then
openclaw message send \
--channel "TRIGGER_SOURCE" \
--text "message" 2>/dev/null || true
else
# Try via a running Docker container
local container
container=$(find_openclaw_containers | head -1)
if [[ -n "$container" ]]; then
docker exec "$container" openclaw message send \
--channel "TRIGGER_SOURCE" \
--text "message" 2>/dev/null || true
fi
fi
# Always print to stdout as a fallback
echo "$message"
}
# ---------------------------------------------------------------------------
# Step 1: Find all running OpenClaw agent processes
# ---------------------------------------------------------------------------
find_openclaw_processes() {
# Look for processes matching common OpenClaw agent patterns.
# We search for multiple patterns to catch agents started different ways.
local pids=()
# Pattern 1: Processes with "openclaw" in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "openclaw[-_ ]agent" 2>/dev/null || true)
# Pattern 2: Processes with "claw-agent" in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "claw-agent" 2>/dev/null || true)
# Pattern 3: Processes with "openclaw-skill" in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "openclaw-skill" 2>/dev/null || true)
# Pattern 4: Processes with "clawdbot" in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "clawdbot" 2>/dev/null || true)
# Pattern 5: Processes with "moltbot" in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "moltbot" 2>/dev/null || true)
# Pattern 6: Processes with OpenClaw gateway in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "openclaw.*gateway" 2>/dev/null || true)
# Pattern 7: Processes matching the OPENCLAW_PROCESS_PATTERN env var
# (allows users to add custom patterns for their specific setup)
if [[ -n "-" ]]; then
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "OPENCLAW_PROCESS_PATTERN" 2>/dev/null || true)
fi
# Deduplicate PIDs (guard against empty array)
if [[ #pids[@] -eq 0 ]]; then
return
fi
printf '%s\n' "pids[@]" | sort -u
}
# ---------------------------------------------------------------------------
# Step 2: Kill all OpenClaw processes
# ---------------------------------------------------------------------------
kill_processes() {
local pids
pids=$(find_openclaw_processes)
local count=0
local killed_pids=()
if [[ -z "$pids" ]]; then
log_event "KILL: No OpenClaw agent processes found."
echo "0"
return
fi
while IFS= read -r pid; do
if [[ "$DRY_RUN" == true ]]; then
log_event "DRY-RUN: Would kill PID $pid ($(ps -p "$pid" -o comm= 2>/dev/null || echo 'unknown'))"
else
# Send SIGTERM first (graceful), then SIGKILL after 5 seconds if still alive
if kill -TERM "$pid" 2>/dev/null; then
killed_pids+=("$pid")
count=$((count + 1))
fi
fi
done <<< "$pids"
# If not a dry run, wait briefly then force-kill any survivors
if [[ "$DRY_RUN" == false && #killed_pids[@] -gt 0 ]]; then
sleep 2
for pid in "killed_pids[@]"; do
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
log_event "KILL: Force-killed stubborn process PID $pid"
fi
done
fi
log_event "KILL: count OpenClaw processes terminated. PIDs: -none"
echo "$count"
}
# ---------------------------------------------------------------------------
# Step 3: Back up and clear OpenClaw cron jobs
# ---------------------------------------------------------------------------
pause_cron_jobs() {
local os
os=$(detect_os)
local cron_count=0
# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"
# Back up the current crontab before touching anything
local backup_file="BACKUP_DIR/deadclaw-crontab-backup-$(date +%Y%m%d-%H%M%S).txt"
if crontab -l &>/dev/null; then
crontab -l > "$backup_file" 2>/dev/null || true
log_event "CRON: Crontab backed up to backup_file"
# Count OpenClaw-related cron entries
cron_count=$(grep -c -i "openclaw\|claw-agent\|clawdbot\|moltbot" "$backup_file" 2>/dev/null || echo "0")
if [[ "$DRY_RUN" == true ]]; then
log_event "DRY-RUN: Would remove cron_count OpenClaw cron entries"
else
# Surgical removal: only OpenClaw-related entries, preserve everything else
crontab -l 2>/dev/null | grep -v -i "openclaw\|claw-agent\|clawdbot\|moltbot" | crontab - 2>/dev/null || true
log_event "CRON: cron_count OpenClaw cron entries removed"
fi
else
log_event "CRON: No crontab found. Nothing to back up or clear."
fi
# Handle OS-specific scheduled tasks
if [[ "$os" == "macos" ]]; then
# Pause any OpenClaw launchd agents
local agents_paused=0
for plist in ~/Library/LaunchAgents/com.openclaw.*; do
[[ -f "$plist" ]] || continue
if [[ "$DRY_RUN" == true ]]; then
log_event "DRY-RUN: Would unload launchd agent: $(basename "$plist")"
else
launchctl unload "$plist" 2>/dev/null || true
log_event "CRON: Unloaded launchd agent: $(basename "$plist")"
fi
agents_paused=$((agents_paused + 1))
done
cron_count=$((cron_count + agents_paused))
elif [[ "$os" == "linux" ]]; then
# Pause any OpenClaw systemd user services
local services_paused=0
while IFS= read -r service; do
[[ -n "$service" ]] || continue
if [[ "$DRY_RUN" == true ]]; then
log_event "DRY-RUN: Would stop systemd service: $service"
else
systemctl --user stop "$service" 2>/dev/null || true
systemctl --user disable "$service" 2>/dev/null || true
log_event "CRON: Stopped systemd service: $service"
fi
services_paused=$((services_paused + 1))
done < <(systemctl --user list-units --type=service --no-legend 2>/dev/null | grep -i "openclaw\|claw-agent\|clawdbot\|moltbot" | awk '{print $1}' || true)
cron_count=$((cron_count + services_paused))
fi
echo "$cron_count"
}
# ---------------------------------------------------------------------------
# Step 4: Kill active OpenClaw sessions
# ---------------------------------------------------------------------------
kill_sessions() {
# If OpenClaw CLI is available on host, use it to terminate active sessions
if command -v openclaw &>/dev/null; then
if [[ "$DRY_RUN" == true ]]; then
local session_list
session_list=$(openclaw session list --format json 2>/dev/null || echo "[]")
log_event "DRY-RUN: Would terminate all active OpenClaw sessions: session_list"
else
openclaw session kill-all 2>/dev/null || true
log_event "SESSIONS: All active OpenClaw sessions terminated"
fi
else
# Docker sessions are already killed in kill_docker() before container stop.
# If there were no Docker containers either, note it.
local containers
containers=$(find_openclaw_containers)
if [[ -z "$containers" ]]; then
log_event "SESSIONS: OpenClaw CLI not found and no Docker containers — skipping session cleanup"
else
log_event "SESSIONS: Sessions killed via Docker (handled in container stop sequence)"
fi
fi
}
# ---------------------------------------------------------------------------
# Step 5: Get current token spend (for the incident log)
# ---------------------------------------------------------------------------
get_token_spend() {
# Try to read token spend from OpenClaw's metrics if available
if command -v openclaw &>/dev/null; then
openclaw metrics token-spend --format plain 2>/dev/null || echo "unknown"
else
# Try via Docker container
local container
container=$(find_openclaw_containers | head -1)
if [[ -n "$container" ]]; then
docker exec "$container" openclaw metrics token-spend --format plain 2>/dev/null || echo "unknown"
elif [[ -f "-/tmp/.openclaw/metrics/tokens.log" ]]; then
tail -1 "-/tmp/.openclaw/metrics/tokens.log" 2>/dev/null || echo "unknown"
else
echo "unknown"
fi
fi
}
# ---------------------------------------------------------------------------
# Main execution
# ---------------------------------------------------------------------------
main() {
local ts
ts=$(timestamp)
echo ""
echo "============================================"
if [[ "$DRY_RUN" == true ]]; then
echo " DeadClaw — DRY RUN (no actions taken)"
else
echo " DeadClaw — Emergency Kill Activated"
fi
echo " ts"
echo "============================================"
echo ""
# Write incident log header
log_event "=========================================="
log_event "DEADCLAW KILL EVENT"
log_event "Trigger source: TRIGGER_SOURCE"
log_event "Trigger method: TRIGGER_METHOD"
log_event "Dry run: DRY_RUN"
log_event "=========================================="
# Execute the kill sequence — both native processes and Docker containers
local processes_killed
processes_killed=$(kill_processes)
local containers_killed
containers_killed=$(kill_docker)
local cron_paused
cron_paused=$(pause_cron_jobs)
kill_sessions
local token_spend
token_spend=$(get_token_spend)
# Combine native + Docker counts for the summary
local total_killed=$((processes_killed + containers_killed))
# Write summary to incident log
log_event "SUMMARY: processes_killed processes killed, containers_killed containers stopped, cron_paused cron jobs paused, token spend: token_spend"
log_event "=========================================="
# Build and send the confirmation message
local prefix=""
[[ "$DRY_RUN" == true ]] && prefix="[DRY-RUN] "
local confirmation
confirmation="prefix🔴 DeadClaw activated. All agents stopped. ts — total_killed killed (processes_killed processes, containers_killed containers). cron_paused cron jobs paused. See deadclaw.log for full report."
send_confirmation "$confirmation"
# Stop the watchdog too (if it's running), since all agents are dead
if [[ "$DRY_RUN" == false ]]; then
"SCRIPT_DIR/watchdog.sh" stop 2>/dev/null || true
fi
}
main "$@"
exit 0
FILE:deadclaw/scripts/restore.sh
#!/usr/bin/env bash
# ============================================================================
# DeadClaw — restore.sh
# Post-kill recovery script. Restores the backed-up crontab and optionally
# restarts previously running agents.
#
# What this script does:
# 1. Shows what will be restored (crontab entries, agent list)
# 2. Waits for explicit confirmation before proceeding
# 3. Restores the most recent crontab backup
# 4. Restarts the watchdog (optional)
# 5. Sends a confirmation message
#
# Usage:
# ./restore.sh # Interactive restore with confirmation prompt
# ./restore.sh --dry-run # Show what would be restored, don't do anything
# ./restore.sh --confirm # Skip the confirmation prompt (for automation)
#
# Safety: This script never restarts anything without confirmation. After a
# kill event, you want to be sure before bringing things back online.
# ============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
LOG_FILE="SKILL_DIR/deadclaw.log"
BACKUP_DIR="SKILL_DIR/backups"
DRY_RUN=false
AUTO_CONFIRM=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--confirm) AUTO_CONFIRM=true; shift ;;
*) shift ;;
esac
done
timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
log_event() {
local msg="$1"
echo "[$(timestamp)] RESTORE: $msg" >> "$LOG_FILE"
}
send_message() {
local message="$1"
if command -v openclaw &>/dev/null; then
openclaw message broadcast --text "message" 2>/dev/null || true
else
# Try via Docker container
local container
container=$(find_openclaw_containers | head -1)
if [[ -n "$container" ]]; then
docker exec "$container" openclaw message broadcast --text "message" 2>/dev/null || true
fi
fi
echo "$message"
}
# Find running OpenClaw Docker containers
find_openclaw_containers() {
if ! command -v docker &>/dev/null; then
return
fi
docker ps --filter "name=openclaw" --format "{{.Names}}" 2>/dev/null || true
}
# Find stopped OpenClaw Docker containers
find_stopped_containers() {
if ! command -v docker &>/dev/null; then
return
fi
docker ps -a --filter "name=openclaw" --filter "status=exited" --format "{{.Names}}" 2>/dev/null || true
}
# ---------------------------------------------------------------------------
# Step 1: Find the most recent crontab backup
# ---------------------------------------------------------------------------
find_latest_backup() {
if [[ ! -d "$BACKUP_DIR" ]]; then
echo ""
return
fi
# Find the newest backup file
ls -t "BACKUP_DIR"/deadclaw-crontab-backup-*.txt 2>/dev/null | head -1
}
# ---------------------------------------------------------------------------
# Step 2: Show what will be restored
# ---------------------------------------------------------------------------
show_restore_plan() {
local backup_file="$1"
echo ""
echo "============================================"
echo " DeadClaw — Restore Plan"
echo "============================================"
echo ""
if [[ -n "$backup_file" && -f "$backup_file" ]]; then
local entry_count
entry_count=$(wc -l < "$backup_file" | tr -d ' ')
echo "Crontab backup found: $(basename "$backup_file")"
echo " Contains entry_count entries:"
echo ""
# Show the crontab entries (indent them for readability)
while IFS= read -r line; do
echo " line"
done < "$backup_file"
echo ""
else
echo "No crontab backup found. Nothing to restore."
echo ""
fi
# Check for any OpenClaw launchd/systemd services that were disabled
local os
os=$(uname -s)
if [[ "$os" == "Darwin" ]]; then
local disabled_agents
disabled_agents=$(find ~/Library/LaunchAgents -name "com.openclaw.*" 2>/dev/null || true)
if [[ -n "$disabled_agents" ]]; then
echo "LaunchAgents to re-enable:"
echo "$disabled_agents" | while read -r f; do echo " $(basename "$f")"; done
echo ""
fi
elif [[ "$os" == "Linux" ]]; then
local disabled_services
disabled_services=$(systemctl --user list-unit-files --type=service 2>/dev/null | grep -i "openclaw\|claw-agent\|clawdbot\|moltbot" | grep "disabled" || true)
if [[ -n "$disabled_services" ]]; then
echo "Systemd services to re-enable:"
echo "$disabled_services" | while read -r line; do echo " $line"; done
echo ""
fi
fi
# Check for stopped Docker containers
local stopped_containers
stopped_containers=$(find_stopped_containers)
if [[ -n "$stopped_containers" ]]; then
echo "Docker containers to restart:"
while IFS= read -r c; do
[[ -n "$c" ]] && echo " $c"
done <<< "$stopped_containers"
echo ""
fi
# Option to restart watchdog
echo "The watchdog will be restarted after restore."
echo ""
}
# ---------------------------------------------------------------------------
# Step 3: Wait for confirmation
# ---------------------------------------------------------------------------
get_confirmation() {
local backup_file="$1"
if [[ "$AUTO_CONFIRM" == true ]]; then
return 0
fi
if [[ "$DRY_RUN" == true ]]; then
echo "[DRY-RUN] Would wait for confirmation here. Skipping."
return 1 # Don't proceed in dry-run
fi
# Build a specific prompt showing what will be restored
local prompt_detail="Restore"
if [[ -n "$backup_file" && -f "$backup_file" ]]; then
local job_count
job_count=$(wc -l < "$backup_file" | tr -d ' ')
# Extract timestamp from filename: deadclaw-crontab-backup-YYYYMMDD-HHMMSS.txt
local backup_ts
backup_ts=$(basename "$backup_file" | sed 's/deadclaw-crontab-backup-//;s/\.txt//')
prompt_detail="Restore job_count cron jobs from backup taken at backup_ts"
fi
echo "============================================"
echo " prompt_detail? (yes/no)"
echo "============================================"
echo ""
read -r response
case "$response" in
confirm|yes|YES|y|Y)
return 0
;;
*)
echo "Restore cancelled."
log_event "Restore cancelled by user."
return 1
;;
esac
}
# ---------------------------------------------------------------------------
# Step 4: Execute the restore
# ---------------------------------------------------------------------------
do_restore() {
local backup_file="$1"
local restored_items=0
log_event "Restore initiated."
# Restore crontab
if [[ -n "$backup_file" && -f "$backup_file" ]]; then
crontab "$backup_file" 2>/dev/null
if [[ $? -eq 0 ]]; then
log_event "Crontab restored from backup_file"
echo "Crontab restored."
restored_items=$((restored_items + 1))
else
log_event "Failed to restore crontab from backup_file"
echo "Warning: Failed to restore crontab."
fi
fi
# Re-enable OS-specific services
local os
os=$(uname -s)
if [[ "$os" == "Darwin" ]]; then
for plist in ~/Library/LaunchAgents/com.openclaw.*; do
[[ -f "$plist" ]] || continue
launchctl load "$plist" 2>/dev/null || true
log_event "Re-enabled launchd agent: $(basename "$plist")"
restored_items=$((restored_items + 1))
done
elif [[ "$os" == "Linux" ]]; then
while IFS= read -r service; do
[[ -n "$service" ]] || continue
local svc_name
svc_name=$(echo "$service" | awk '{print $1}')
systemctl --user enable "$svc_name" 2>/dev/null || true
systemctl --user start "$svc_name" 2>/dev/null || true
log_event "Re-enabled systemd service: svc_name"
restored_items=$((restored_items + 1))
done < <(systemctl --user list-unit-files --type=service 2>/dev/null | grep -i "openclaw\|claw-agent\|clawdbot\|moltbot" | grep "disabled" || true)
fi
# Restart stopped Docker containers
local stopped_containers
stopped_containers=$(find_stopped_containers)
if [[ -n "$stopped_containers" ]]; then
while IFS= read -r container; do
[[ -n "$container" ]] || continue
docker start "$container" 2>/dev/null || true
log_event "Restarted Docker container: container"
echo "Docker container restarted: container"
restored_items=$((restored_items + 1))
done <<< "$stopped_containers"
# Wait for containers to be ready
echo "Waiting for containers to initialize..."
sleep 5
fi
# Attempt to restart the OpenClaw gateway process
if command -v openclaw &>/dev/null; then
if openclaw gateway start 2>/dev/null; then
log_event "OpenClaw gateway restarted."
echo "OpenClaw gateway restarted."
restored_items=$((restored_items + 1))
else
log_event "Failed to restart OpenClaw gateway (may need manual start)."
echo "Warning: Could not restart OpenClaw gateway. Start it manually."
fi
else
# Try via Docker container (check running containers now)
local running_container
running_container=$(find_openclaw_containers | head -1)
if [[ -n "$running_container" ]]; then
log_event "OpenClaw gateway running inside Docker container: running_container"
echo "OpenClaw gateway running inside Docker container: running_container"
else
log_event "OpenClaw CLI not found and no Docker containers — skipping gateway restart."
echo "Note: OpenClaw CLI not found. Start the gateway manually."
fi
fi
# Restart the watchdog
"SCRIPT_DIR/watchdog.sh" start 2>/dev/null || true
restored_items=$((restored_items + 1))
log_event "Restore complete. restored_items items restored."
# Send confirmation
local msg="DeadClaw restore complete. restored_items items restored. Watchdog restarted. All systems nominal."
send_message "$msg"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
local backup_file
backup_file=$(find_latest_backup)
show_restore_plan "$backup_file"
if [[ "$DRY_RUN" == true ]]; then
echo "[DRY-RUN] No changes made."
exit 0
fi
if get_confirmation "$backup_file"; then
do_restore "$backup_file"
fi
}
main "$@"
FILE:deadclaw/scripts/status.sh
#!/usr/bin/env bash
# ============================================================================
# DeadClaw — status.sh
# Health report script. Sends a plain-English summary of what's running,
# how long it's been running, current token spend rate, and watchdog status.
#
# Supports both native and Docker-based OpenClaw installs.
#
# Usage:
# ./status.sh # Print status to stdout
# ./status.sh --dry-run # Same behavior (status is read-only)
# ./status.sh --json # Output as JSON for programmatic use
#
# Trigger: user sends "status" to any connected OpenClaw channel.
# ============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
PID_FILE="SKILL_DIR/deadclaw-watchdog.pid"
OUTPUT_JSON=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--json) OUTPUT_JSON=true; shift ;;
--dry-run) shift ;; # status is already read-only
*) shift ;;
esac
done
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
# Cross-platform: get elapsed time in seconds for a PID.
get_elapsed_seconds() {
local pid="$1"
local seconds
seconds=$(ps -o etimes= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ -n "$seconds" && "$seconds" =~ ^[0-9]+$ ]]; then
echo "$seconds"
return
fi
local etime
etime=$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ -z "$etime" ]]; then
echo "0"
return
fi
local days=0 hours=0 mins=0 secs=0
if [[ "$etime" == *-* ]]; then
days="etime%%-*"
etime="etime#*-"
fi
IFS=: read -ra parts <<< "$etime"
case #parts[@] in
3) hours="parts[0]"; mins="parts[1]"; secs="parts[2]" ;;
2) mins="parts[0]"; secs="parts[1]" ;;
1) secs="parts[0]" ;;
esac
days=$((10#$days)) hours=$((10#$hours)) mins=$((10#$mins)) secs=$((10#$secs))
echo $(( days*86400 + hours*3600 + mins*60 + secs ))
}
# Format seconds into human-readable uptime
format_uptime() {
local elapsed="$1"
if [[ "$elapsed" -ge 86400 ]]; then
echo "$((elapsed / 86400))d $((elapsed % 86400 / 3600))h"
elif [[ "$elapsed" -ge 3600 ]]; then
echo "$((elapsed / 3600))h $((elapsed % 3600 / 60))m"
elif [[ "$elapsed" -ge 60 ]]; then
echo "$((elapsed / 60))m $((elapsed % 60))s"
else
echo "elapseds"
fi
}
# All OpenClaw process patterns in one place
OPENCLAW_PGREP_PATTERN="openclaw[-_ ]agent|claw-agent|openclaw-skill|clawdbot|moltbot|openclaw.*gateway"
# Find running OpenClaw Docker containers
find_openclaw_containers() {
if ! command -v docker &>/dev/null; then
return
fi
docker ps --filter "name=openclaw" --format "{{.Names}}" 2>/dev/null || true
}
# ---------------------------------------------------------------------------
# Gather information — Docker mode
# ---------------------------------------------------------------------------
gather_docker_status() {
local containers
containers=$(find_openclaw_containers)
if [[ -z "$containers" ]]; then
return 1
fi
# List containers with uptime
echo ""
echo "DeadClaw Status Report"
echo "======================"
echo ""
local container_count=0
while IFS= read -r container; do
[[ -n "$container" ]] || continue
container_count=$((container_count + 1))
local status uptime_str
status=$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || echo "unknown")
uptime_str=$(docker inspect --format '{{.State.StartedAt}}' "$container" 2>/dev/null || echo "unknown")
echo "Docker container: container"
echo " Status: status"
echo " Started: uptime_str"
echo ""
# Get detailed status from inside the container
local oc_status
oc_status=$(docker exec "$container" openclaw status --all 2>/dev/null || echo "(could not reach openclaw inside container)")
echo " OpenClaw status:"
echo "$oc_status" | while IFS= read -r line; do
echo " $line"
done
echo ""
done <<< "$containers"
echo "Containers: container_count running"
echo ""
# Also check for native processes on the host
local native_pids
native_pids=$(pgrep -f "OPENCLAW_PGREP_PATTERN" 2>/dev/null || true)
if [[ -n "$native_pids" ]]; then
local native_count
native_count=$(echo "$native_pids" | wc -l | tr -d ' ')
echo "Host processes: native_count running"
while IFS= read -r pid; do
[[ -n "$pid" ]] || continue
local pname elapsed uptime
pname=$(ps -o comm= -p "$pid" 2>/dev/null || echo "unknown")
elapsed=$(get_elapsed_seconds "$pid")
uptime=$(format_uptime "$elapsed")
echo " - pname (PID pid) — up uptime"
done <<< "$native_pids"
echo ""
fi
# Watchdog status
if [[ -f "$PID_FILE" ]]; then
local wpid
wpid=$(cat "$PID_FILE" 2>/dev/null)
if kill -0 "$wpid" 2>/dev/null; then
echo "Watchdog: Active (PID wpid)"
else
echo "Watchdog: Not running (stale PID file)"
fi
else
echo "Watchdog: Not running"
fi
echo ""
return 0
}
# ---------------------------------------------------------------------------
# Gather information — Native mode (no Docker)
# ---------------------------------------------------------------------------
gather_native_status() {
declare -a agent_names=()
declare -a agent_pids=()
declare -a agent_uptimes=()
while IFS= read -r pid; do
[[ -n "$pid" ]] || continue
agent_pids+=("$pid")
local_name=$(ps -o comm= -p "$pid" 2>/dev/null || echo "unknown")
agent_names+=("$local_name")
local_elapsed=$(get_elapsed_seconds "$pid")
agent_uptimes+=("$(format_uptime "$local_elapsed")")
done < <(pgrep -f "OPENCLAW_PGREP_PATTERN" 2>/dev/null || true)
local agent_count=#agent_pids[@]
# Token spend rate
local token_rate="unknown"
local token_total="unknown"
local token_log="-/tmp/.openclaw/metrics/tokens.log"
if [[ -f "$token_log" ]]; then
local_total=$(tail -50 "$token_log" 2>/dev/null | awk '{sum += $2} END {print sum+0}' || echo "0")
token_total="$local_total"
token_rate="~local_total tokens/10min"
fi
# Watchdog status
local watchdog_running=false
local watchdog_pid=""
if [[ -f "$PID_FILE" ]]; then
watchdog_pid=$(cat "$PID_FILE" 2>/dev/null)
if kill -0 "$watchdog_pid" 2>/dev/null; then
watchdog_running=true
fi
fi
# Warnings
local warnings=()
local max_runtime=-30
local max_tokens=-50000
for i in "!agent_pids[@]"; do
local_elapsed=$(get_elapsed_seconds "agent_pids[$i]")
local_threshold_sec=$((max_runtime * 60))
local_warning_sec=$((local_threshold_sec * 80 / 100))
if [[ "$local_elapsed" -ge "$local_warning_sec" ]]; then
warnings+=("Agent agent_names[$i] (PID agent_pids[$i]) has been running agent_uptimes[$i] — approaching max_runtimemin kill threshold")
fi
done
if [[ "$token_total" != "unknown" && "$token_total" -ge $((max_tokens * 80 / 100)) ]]; then
warnings+=("Token spend is at token_total — approaching max_tokens kill threshold")
fi
if [[ "$OUTPUT_JSON" == true ]]; then
local agents_json="["
for i in "!agent_pids[@]"; do
[[ $i -gt 0 ]] && agents_json+=","
agents_json+="{\"name\":\"agent_names[$i]\",\"pid\":agent_pids[$i],\"uptime\":\"agent_uptimes[$i]\"}"
done
agents_json+="]"
local warnings_json="["
for i in "!warnings[@]"; do
[[ $i -gt 0 ]] && warnings_json+=","
warnings_json+="\"warnings[$i]\""
done
warnings_json+="]"
cat <<EOF
{
"agents_running": agent_count,
"agents": agents_json,
"token_rate": "token_rate",
"watchdog_active": watchdog_running,
"watchdog_pid": "watchdog_pid",
"warnings": warnings_json
}
EOF
else
echo ""
echo "DeadClaw Status Report"
echo "======================"
echo ""
if [[ "$agent_count" -eq 0 ]]; then
echo "Agents: None running."
else
echo "Agents: agent_count running"
for i in "!agent_pids[@]"; do
echo " - agent_names[$i] (PID agent_pids[$i]) — up agent_uptimes[$i]"
done
fi
echo ""
echo "Token spend: token_rate"
echo ""
if [[ "$watchdog_running" == true ]]; then
echo "Watchdog: Active (PID watchdog_pid)"
else
echo "Watchdog: Not running"
fi
if [[ #warnings[@] -gt 0 ]]; then
echo ""
echo "Warnings:"
for w in "warnings[@]"; do
echo " ! w"
done
fi
echo ""
fi
}
# ---------------------------------------------------------------------------
# Main — detect Docker vs native and gather status
# ---------------------------------------------------------------------------
containers=$(find_openclaw_containers)
if [[ -n "$containers" ]]; then
gather_docker_status
else
gather_native_status
fi
FILE:deadclaw/scripts/watchdog.sh
#!/usr/bin/env bash
# ============================================================================
# DeadClaw — watchdog.sh
# Background monitor daemon that auto-triggers the kill script when it detects
# dangerous conditions in running OpenClaw agents.
#
# What this script monitors (every 60 seconds):
# 1. Runaway loops — any agent running longer than 30 minutes
# 2. Token burn — token spend exceeding 50,000 in under 10 minutes
# 3. Unauthorized network calls — outbound connections to non-whitelisted domains
# 4. Sandbox escape — file writes outside the designated workspace
#
# Usage:
# ./watchdog.sh start # Start the watchdog daemon
# ./watchdog.sh stop # Stop the watchdog daemon
# ./watchdog.sh status # Check if watchdog is running
# ./watchdog.sh start --dry-run # Monitor and log but don't auto-kill
#
# The watchdog writes a PID file so it can be stopped and restarted cleanly.
# ============================================================================
set -uo pipefail
# ---------------------------------------------------------------------------
# Configuration — all overridable via environment variables
# ---------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
LOG_FILE="SKILL_DIR/deadclaw.log"
PID_FILE="SKILL_DIR/deadclaw-watchdog.pid"
# Thresholds (with sensible defaults)
MAX_RUNTIME_MIN="-30"
MAX_TOKENS="-50000"
TOKEN_WINDOW_MIN="-10"
WHITELIST_FILE="-${SKILL_DIR/network-whitelist.txt}"
WORKSPACE="-${OPENCLAW_WORKSPACE:-}"
CHECK_INTERVAL=60 # seconds between checks
DRY_RUN=false
# ---------------------------------------------------------------------------
# Parse command-line arguments
# ---------------------------------------------------------------------------
ACTION="-"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
shift
;;
*)
shift
;;
esac
done
# ---------------------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------------------
timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
# Compact log line for watchdog checks (keeps the log small)
wlog() {
echo "[$(timestamp)] WATCHDOG: $1" >> "$LOG_FILE"
}
# Cross-platform helper: get elapsed time in seconds for a PID.
# Linux has ps -o etimes= (seconds directly). macOS only has ps -o etime=
# which returns [[DD-]HH:]MM:SS format that we need to parse.
get_elapsed_seconds() {
local pid="$1"
# Try Linux-style etimes first (returns seconds directly)
local seconds
seconds=$(ps -o etimes= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ -n "$seconds" && "$seconds" =~ ^[0-9]+$ ]]; then
echo "$seconds"
return
fi
# Fall back to etime format: [[DD-]HH:]MM:SS
local etime
etime=$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ -z "$etime" ]]; then
echo "0"
return
fi
local days=0 hours=0 mins=0 secs=0
# Split off days if present (format: DD-HH:MM:SS)
if [[ "$etime" == *-* ]]; then
days="etime%%-*"
etime="etime#*-"
fi
# Split remaining HH:MM:SS or MM:SS or SS
IFS=: read -ra parts <<< "$etime"
case #parts[@] in
3) hours="parts[0]"; mins="parts[1]"; secs="parts[2]" ;;
2) mins="parts[0]"; secs="parts[1]" ;;
1) secs="parts[0]" ;;
esac
# Strip leading zeros to avoid octal interpretation
days=$((10#$days)) hours=$((10#$hours)) mins=$((10#$mins)) secs=$((10#$secs))
echo $(( days*86400 + hours*3600 + mins*60 + secs ))
}
# All OpenClaw process patterns in one place
OPENCLAW_PGREP_PATTERN="openclaw[-_ ]agent|claw-agent|openclaw-skill|clawdbot|moltbot|openclaw.*gateway"
# Find running OpenClaw Docker containers
find_openclaw_containers() {
if ! command -v docker &>/dev/null; then
return
fi
docker ps --filter "name=openclaw" --format "{{.Names}}" 2>/dev/null || true
}
# Trigger the kill script with a reason
trigger_kill() {
local reason="$1"
wlog "AUTO-TRIGGER: reason"
local dry_flag=""
[[ "$DRY_RUN" == true ]] && dry_flag="--dry-run"
"SCRIPT_DIR/kill.sh" \
--trigger-source "watchdog" \
--trigger-method "auto" \
$dry_flag
# Send the specific watchdog alert message
local prefix=""
[[ "$DRY_RUN" == true ]] && prefix="[DRY-RUN] "
local alert="prefix🔴 DeadClaw auto-triggered. Reason: reason. All processes stopped. Check deadclaw.log."
if command -v openclaw &>/dev/null; then
openclaw message broadcast --text "alert" 2>/dev/null || true
fi
echo "$alert"
}
# ---------------------------------------------------------------------------
# Check 1: Runaway loops — agents running longer than the max threshold
# ---------------------------------------------------------------------------
check_runtime() {
local max_seconds=$((MAX_RUNTIME_MIN * 60))
# Check native OpenClaw agent processes
while IFS= read -r pid; do
[[ -n "$pid" ]] || continue
local elapsed
elapsed=$(get_elapsed_seconds "$pid")
if [[ "$elapsed" -gt "$max_seconds" ]]; then
local cmd
cmd=$(ps -o comm= -p "$pid" 2>/dev/null || echo "unknown")
local runtime_min=$((elapsed / 60))
trigger_kill "agent loop exceeded MAX_RUNTIME_MINmin threshold (PID pid, cmd, running for runtime_minmin)"
return 1
fi
done < <(pgrep -f "OPENCLAW_PGREP_PATTERN" 2>/dev/null || true)
# Check Docker container sessions
local containers
containers=$(find_openclaw_containers)
if [[ -n "$containers" ]]; then
while IFS= read -r container; do
[[ -n "$container" ]] || continue
# Get session ages from inside the container via openclaw status
# Parse the sessions table for age values like "27m ago", "2h ago"
local session_info
session_info=$(docker exec "$container" openclaw status 2>/dev/null || true)
# Extract session age lines — look for patterns like "27m ago" or "2h ago"
local ages
ages=$(echo "$session_info" | grep -oP '\d+[hm]\s+ago' 2>/dev/null || true)
while IFS= read -r age_str; do
[[ -n "$age_str" ]] || continue
local age_min=0
if [[ "$age_str" =~ ^([0-9]+)h ]]; then
age_min=$(( BASH_REMATCH[1] * 60 ))
elif [[ "$age_str" =~ ^([0-9]+)m ]]; then
age_min=BASH_REMATCH[1]
fi
if [[ "$age_min" -gt "$MAX_RUNTIME_MIN" ]]; then
trigger_kill "agent session in container container exceeded MAX_RUNTIME_MINmin threshold (running for age_minmin)"
return 1
fi
done <<< "$ages"
done <<< "$containers"
fi
return 0
}
# ---------------------------------------------------------------------------
# Check 2: Token burn — excessive token spend in a short window
# ---------------------------------------------------------------------------
check_token_spend() {
local token_log="-/tmp/.openclaw/metrics/tokens.log"
# In Docker mode, try to get token info from containers
if [[ ! -f "$token_log" ]]; then
local containers
containers=$(find_openclaw_containers)
if [[ -n "$containers" ]]; then
while IFS= read -r container; do
[[ -n "$container" ]] || continue
# Try to get token spend from the container's openclaw status
local status_output
status_output=$(docker exec "$container" openclaw status 2>/dev/null || true)
# Look for token info like "0.0k/400k (0%)" in the status output
local token_match
token_match=$(echo "$status_output" | grep -oP '[\d.]+k/[\d.]+k\s*\(\d+%\)' 2>/dev/null | head -1 || true)
if [[ -n "$token_match" ]]; then
# Extract used tokens (the first number before /)
local used_k
used_k=$(echo "$token_match" | grep -oP '^[\d.]+' || echo "0")
# Convert k to actual tokens (rough)
local used_tokens
used_tokens=$(echo "$used_k * 1000" | bc 2>/dev/null || echo "0")
used_tokens=used_tokens%.* # truncate decimal
if [[ "$used_tokens" -gt "$MAX_TOKENS" ]]; then
trigger_kill "token spend exceeded MAX_TOKENS tokens in container container (actual: ~used_tokens tokens)"
return 1
fi
fi
done <<< "$containers"
fi
return 0
fi
# If the token log doesn't exist and no Docker, skip silently
[[ -f "$token_log" ]] || return 0
local window_seconds=$((TOKEN_WINDOW_MIN * 60))
local cutoff
cutoff=$(date -u -d "window_seconds seconds ago" +%s 2>/dev/null || \
date -u -v-window_secondsS +%s 2>/dev/null || echo "0")
# Token log format expected: TIMESTAMP TOKENS_USED
# Sum up tokens used within the window
local total_tokens=0
while IFS=' ' read -r ts tokens; do
[[ -n "$ts" && -n "$tokens" ]] || continue
local entry_epoch
entry_epoch=$(date -u -d "$ts" +%s 2>/dev/null || date -u -jf "%Y-%m-%dT%H:%M:%SZ" "$ts" +%s 2>/dev/null || echo "0")
if [[ "$entry_epoch" -ge "$cutoff" ]]; then
total_tokens=$((total_tokens + tokens))
fi
done < "$token_log"
if [[ "$total_tokens" -gt "$MAX_TOKENS" ]]; then
trigger_kill "token spend exceeded MAX_TOKENS tokens in TOKEN_WINDOW_MINmin window (actual: total_tokens tokens)"
return 1
fi
return 0
}
# ---------------------------------------------------------------------------
# Check 3: Unauthorized network calls — connections to non-whitelisted domains
# ---------------------------------------------------------------------------
check_network() {
# Skip if no whitelist file exists (user hasn't configured network restrictions)
[[ -f "$WHITELIST_FILE" ]] || return 0
# Get current outbound connections from OpenClaw agent processes (native)
local agent_pids
agent_pids=$(pgrep -f "OPENCLAW_PGREP_PATTERN" 2>/dev/null || true)
# In Docker mode, also check container network connections
if [[ -z "$agent_pids" ]]; then
local containers
containers=$(find_openclaw_containers)
if [[ -n "$containers" ]]; then
# For Docker containers, we check from the host using docker's network
while IFS= read -r container; do
[[ -n "$container" ]] || continue
local container_pid
container_pid=$(docker inspect --format '{{.State.Pid}}' "$container" 2>/dev/null || true)
[[ -n "$container_pid" && "$container_pid" != "0" ]] && agent_pids="agent_pids+$'\n'container_pid"
done <<< "$containers"
fi
fi
[[ -n "$agent_pids" ]] || return 0
# Load whitelist into an associative array for fast lookup
declare -A allowed_domains
while IFS= read -r domain; do
domain=$(echo "$domain" | tr -d '[:space:]')
[[ -n "$domain" && "$domain" != \#* ]] && allowed_domains["$domain"]=1
done < "$WHITELIST_FILE"
# Check each agent process for outbound connections
while IFS= read -r pid; do
[[ -n "$pid" ]] || continue
# Use lsof or ss to find network connections for this PID
local connections
connections=$(lsof -i -a -p "$pid" -n 2>/dev/null | grep "ESTABLISHED" || true)
while IFS= read -r conn; do
[[ -n "$conn" ]] || continue
# Extract the remote address/hostname
local remote
remote=$(echo "$conn" | awk '{print $9}' | cut -d':' -f1 | sed 's/->/ /g' | awk '{print $NF}')
[[ -n "$remote" ]] || continue
# Try reverse DNS lookup
local hostname
hostname=$(host "$remote" 2>/dev/null | awk '/domain name pointer/ {print $NF}' | sed 's/\.$//' || echo "$remote")
# Check if this domain (or any parent domain) is whitelisted
local is_allowed=false
for allowed in "!allowed_domains[@]"; do
if [[ "$hostname" == *"$allowed"* || "$remote" == *"$allowed"* ]]; then
is_allowed=true
break
fi
done
if [[ "$is_allowed" == false ]]; then
trigger_kill "unauthorized outbound network call to hostname (remote) from PID pid"
return 1
fi
done <<< "$connections"
done <<< "$agent_pids"
return 0
}
# ---------------------------------------------------------------------------
# Check 4: Sandbox escape — file writes outside the designated workspace
# ---------------------------------------------------------------------------
check_file_writes() {
# Skip if no workspace is configured
[[ -n "$WORKSPACE" ]] || return 0
local agent_pids
agent_pids=$(pgrep -f "OPENCLAW_PGREP_PATTERN" 2>/dev/null || true)
[[ -n "$agent_pids" ]] || return 0
# Use lsof to check for open file descriptors in write mode outside workspace
while IFS= read -r pid; do
[[ -n "$pid" ]] || continue
# Find files opened for writing by this process
local write_files
write_files=$(lsof -p "$pid" 2>/dev/null | awk '$4 ~ /[0-9]+w/' | awk '{print $9}' || true)
while IFS= read -r filepath; do
[[ -n "$filepath" ]] || continue
# Resolve to absolute path
local abs_path
abs_path=$(realpath "$filepath" 2>/dev/null || echo "$filepath")
# Check if the file is outside the workspace (allow /tmp and /dev)
if [[ "$abs_path" != "WORKSPACE"* && \
"$abs_path" != /tmp* && \
"$abs_path" != /dev* && \
"$abs_path" != /proc* ]]; then
trigger_kill "process PID pid writing outside workspace: abs_path (workspace: WORKSPACE)"
return 1
fi
done <<< "$write_files"
done <<< "$agent_pids"
return 0
}
# ---------------------------------------------------------------------------
# Daemon management
# ---------------------------------------------------------------------------
start_watchdog() {
# Check if already running
if [[ -f "$PID_FILE" ]]; then
local existing_pid
existing_pid=$(cat "$PID_FILE")
if kill -0 "$existing_pid" 2>/dev/null; then
echo "Watchdog is already running (PID existing_pid)."
exit 0
else
# Stale PID file — clean it up
rm -f "$PID_FILE"
fi
fi
echo "Starting DeadClaw watchdog..."
wlog "Watchdog starting. Check interval: CHECK_INTERVALs. Dry run: DRY_RUN"
wlog "Thresholds: runtime=MAX_RUNTIME_MINmin, tokens=MAX_TOKENS/TOKEN_WINDOW_MINmin"
# Fork into the background
(
# Clean up PID file on exit (TERM from kill command, INT from Ctrl-C)
trap 'wlog "Watchdog stopped by signal."; rm -f "$PID_FILE"; exit 0' TERM INT
while true; do
# Run all checks. If any check triggers a kill, the watchdog stops
# (because kill.sh stops the watchdog as part of its cleanup).
wlog "CHECK: scanning..."
check_runtime || { wlog "Watchdog stopping after auto-trigger."; exit 0; }
check_token_spend || { wlog "Watchdog stopping after auto-trigger."; exit 0; }
check_network || { wlog "Watchdog stopping after auto-trigger."; exit 0; }
check_file_writes || { wlog "Watchdog stopping after auto-trigger."; exit 0; }
wlog "CHECK: all clear"
# Sleep in background + wait makes the loop interruptible by signals.
# A plain 'sleep 60' blocks signal delivery until it completes.
sleep "$CHECK_INTERVAL" &
wait $! 2>/dev/null || true
done
) &
local daemon_pid=$!
echo "$daemon_pid" > "$PID_FILE"
echo "Watchdog started (PID daemon_pid). Logging to LOG_FILE."
}
stop_watchdog() {
if [[ -f "$PID_FILE" ]]; then
local pid
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
wlog "Watchdog stopped (PID pid)"
echo "Watchdog stopped (PID pid)."
else
echo "Watchdog was not running (stale PID file)."
fi
rm -f "$PID_FILE"
else
echo "Watchdog is not running (no PID file found)."
fi
}
watchdog_status() {
if [[ -f "$PID_FILE" ]]; then
local pid
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "Watchdog is running (PID pid)."
echo " Check interval: CHECK_INTERVALs"
echo " Max runtime: MAX_RUNTIME_MINmin"
echo " Max tokens: MAX_TOKENS / TOKEN_WINDOW_MINmin"
echo " Dry run: DRY_RUN"
return 0
else
echo "Watchdog is not running (stale PID file)."
rm -f "$PID_FILE"
return 1
fi
else
echo "Watchdog is not running."
return 1
fi
}
# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------
case "ACTION" in
start)
start_watchdog
;;
stop)
stop_watchdog
;;
status)
watchdog_status
;;
*)
echo "Usage: watchdog.sh {start|stop|status} [--dry-run]"
exit 1
;;
esac
FILE:deadclaw/SKILL.md
---
name: deadclaw
description: >
Emergency kill switch for OpenClaw agents. Instantly halts all running agents,
pauses scheduled jobs, kills active sessions, and logs everything — triggered by
a single message, a WebChat button, or a phone home screen shortcut. Includes a
background watchdog that auto-kills agents on runaway loops, excessive token spend,
unauthorized network calls, or out-of-bounds file writes. Use this skill whenever
the user mentions: emergency stop, kill switch, stop agents, halt agents, panic
button, deadclaw, agent safety, runaway agent, kill all, stop everything, or any
urgent need to shut down OpenClaw processes immediately.
version: 1.0.0
author: "Kintupercy"
trigger_keywords:
- kill
- KILL
- dead
- deadclaw
- stop everything
- emergency stop
- "🔴"
- status
- restore
allowed-tools:
- bash
- messaging
- webchat
---
# DeadClaw — Emergency Kill Switch for OpenClaw Agents
> "One tap. Everything stops."
DeadClaw is a single-purpose emergency kill switch. When triggered, it immediately
halts all running OpenClaw agent processes, pauses all scheduled cron jobs and
heartbeats, kills all active sessions, and writes a timestamped incident log. It
then confirms back to whichever surface triggered it.
## Why DeadClaw Exists
The ClawHavoc attack (February 2026) exposed 1,184 malicious skills in the OpenClaw
ecosystem. People run agents autonomously — often overnight, often unattended. When
something goes wrong, you need a way to stop everything from wherever you are, even
from your phone. DeadClaw is that way.
---
## Activation Methods
DeadClaw supports three activation methods. All three execute the exact same kill
sequence — the only difference is how the user triggers it.
### Method 1: Message Trigger
The user sends a trigger word to any connected OpenClaw channel (Telegram, WhatsApp,
Discord, Slack, or any other connected channel). The following words activate DeadClaw:
- `kill` or `KILL`
- `dead`
- `stop everything`
- `emergency stop`
- `deadclaw`
- `🔴`
When a trigger word is detected:
1. Execute `scripts/kill.sh` from the DeadClaw skill directory
2. Capture the output (process count, cron jobs paused, timestamp)
3. Send confirmation back to the **same channel** the trigger came from:
`🔴 DeadClaw activated. All agents stopped. [timestamp] — [X] processes killed. [X] cron jobs paused. See deadclaw.log for full report.`
### Method 2: WebChat Kill Button
A persistent red button rendered in the OpenClaw WebChat dashboard. The HTML widget
is located at `ui/deadclaw-button.html`. It calls `kill.sh` via OpenClaw's WebChat
API hooks (`window.OpenClaw.exec()`). If the WebChat hooks are unavailable, the
button degrades to showing an error message with manual instructions.
To embed the button, use OpenClaw's WebChat customization hooks:
```javascript
OpenClaw.WebChat.registerWidget('deadclaw-button', {
src: 'skills/deadclaw/ui/deadclaw-button.html',
position: 'top-bar',
persistent: true
});
```
### Method 3: Phone Home Screen Shortcut
A pre-built shortcut that sends the kill trigger message (`deadclaw`) to the user's
configured Telegram bot. Setup guides for iOS and Android are in `docs/`:
- `docs/iphone-shortcut-guide.md` — iOS Shortcuts setup
- `docs/android-widget-guide.md` — Android widget setup (Tasker or HTTP Shortcuts)
---
## Watchdog (Passive Protection)
DeadClaw includes a background watchdog (`scripts/watchdog.sh`) that monitors for
dangerous conditions and auto-triggers the kill without any user action.
The watchdog checks every 60 seconds for:
1. **Runaway loops** — Any agent process running longer than 30 minutes continuously
2. **Token burn** — Token spend exceeding 50,000 tokens in under 10 minutes
3. **Unauthorized network** — Outbound network calls to domains not on the user's whitelist
4. **Sandbox escape** — Any process attempting to write outside the designated OpenClaw workspace
When the watchdog auto-triggers, it sends an alert explaining the reason:
`🔴 DeadClaw auto-triggered. Reason: [specific reason]. All processes stopped. Check deadclaw.log.`
### Watchdog Configuration
The watchdog reads its thresholds from environment variables (with sensible defaults):
| Variable | Default | Description |
|---|---|---|
| `DEADCLAW_MAX_RUNTIME_MIN` | 30 | Max agent runtime in minutes before auto-kill |
| `DEADCLAW_MAX_TOKENS` | 50000 | Max token spend in the monitoring window |
| `DEADCLAW_TOKEN_WINDOW_MIN` | 10 | Token spend monitoring window in minutes |
| `DEADCLAW_WHITELIST` | `./network-whitelist.txt` | Allowed outbound domains (one per line) |
| `DEADCLAW_WORKSPACE` | `$OPENCLAW_WORKSPACE` | Designated workspace directory |
Start the watchdog:
```bash
scripts/watchdog.sh start
```
Stop the watchdog:
```bash
scripts/watchdog.sh stop
```
---
## Additional Commands
### Status Check
User sends `status` to any connected channel. DeadClaw responds with a plain-English
health report by executing `scripts/status.sh`:
- What agents are currently running (name, PID, uptime)
- Current token spend rate
- Whether the watchdog is active
- Any warnings about approaching thresholds
### Restore After Kill
User sends `restore` to any connected channel. DeadClaw executes `scripts/restore.sh`,
which:
1. Shows what will be restored (backed-up crontab entries, disabled services)
2. Prompts: "Restore [X] cron jobs from backup taken at [timestamp]? (yes/no)"
3. Restores the crontab from the most recent backup
4. Attempts to restart the OpenClaw gateway
5. Restarts the watchdog
6. Confirms restoration with a summary
---
## Scripts Reference
| Script | Purpose |
|---|---|
| `scripts/kill.sh` | Core kill script — stops all agents, pauses cron, logs incident |
| `scripts/watchdog.sh` | Background monitor daemon — auto-triggers kill on threshold breach |
| `scripts/status.sh` | Health report — shows running agents, token spend, watchdog status |
| `scripts/restore.sh` | Post-kill recovery — restores crontab and restarts agents |
All scripts support a `--dry-run` flag that logs what would happen without taking action.
---
## Incident Log
All kill events are logged to `deadclaw.log` in the skill directory. Each entry
records: timestamp, trigger source (channel name), trigger method (message/button/
watchdog/auto), processes killed (count and PIDs), cron jobs paused, and token spend
at time of kill. The log is append-only and never automatically cleared.
FILE:deadclaw/ui/deadclaw-button.html
<!DOCTYPE html>
<!--
DeadClaw — WebChat Kill Button Widget
A persistent red button for the OpenClaw WebChat dashboard. When clicked,
it calls kill.sh via OpenClaw's WebChat API hooks and shows confirmation.
Embedding:
OpenClaw.WebChat.registerWidget('deadclaw-button', {
src: 'skills/deadclaw/ui/deadclaw-button.html',
position: 'top-bar',
persistent: true
});
If WebChat hooks are unavailable, the button shows an error with manual
instructions instead of silently failing.
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeadClaw Kill Switch</title>
<style>
/* ----------------------------------------------------------------
DeadClaw button — dark, stark, high contrast.
This is a security tool. The design communicates urgency.
---------------------------------------------------------------- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
background: #0a0a0a;
color: #e0e0e0;
min-height: 60px;
}
.deadclaw-container {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 12px;
gap: 12px;
background: #0a0a0a;
border-bottom: 1px solid #1a1a1a;
}
/* Pulsing red dot — shows agent activity */
.status-indicator {
position: relative;
width: 12px;
height: 12px;
flex-shrink: 0;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #cc0000;
}
.status-dot.active {
background: #ff0000;
animation: pulse 2s ease-in-out infinite;
}
.status-dot.idle {
background: #444;
animation: none;
}
.status-dot.killed {
background: #ff0000;
animation: none;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
box-shadow: 0 0 4px rgba(255, 0, 0, 0.4);
}
50% {
opacity: 0.5;
box-shadow: 0 0 12px rgba(255, 0, 0, 0.8);
}
}
/* The kill button itself */
.kill-button {
flex: 1;
max-width: 600px;
padding: 12px 24px;
background: #cc0000;
color: #ffffff;
border: 2px solid #ff0000;
border-radius: 4px;
font-family: inherit;
font-size: 14px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
cursor: pointer;
transition: background 0.15s ease, transform 0.1s ease;
user-select: none;
}
.kill-button:hover {
background: #e60000;
transform: scale(1.01);
}
.kill-button:active {
background: #990000;
transform: scale(0.99);
}
.kill-button:disabled {
background: #333;
border-color: #555;
color: #888;
cursor: not-allowed;
transform: none;
}
/* Confirmation state — after a successful kill */
.kill-button.confirmed {
background: #1a1a1a;
border-color: #ff0000;
color: #ff0000;
cursor: default;
}
/* Error state — when WebChat hooks aren't available */
.kill-button.error {
background: #1a1a1a;
border-color: #ff6600;
color: #ff6600;
}
/* Status text below the button */
.status-text {
font-size: 11px;
color: #666;
text-align: center;
padding-top: 4px;
font-family: monospace;
min-height: 16px;
}
.status-text.alert {
color: #ff0000;
}
.status-text.success {
color: #00cc44;
}
</style>
</head>
<body>
<div class="deadclaw-container">
<div class="status-indicator">
<div class="status-dot active" id="statusDot"></div>
</div>
<button class="kill-button" id="killButton" onclick="handleKill()">
Kill All Agents
</button>
</div>
<div class="status-text" id="statusText">DeadClaw armed</div>
<script>
// -----------------------------------------------------------------------
// DeadClaw WebChat Button Logic
//
// This script tries to use OpenClaw's WebChat API hooks to execute the
// kill script. If those hooks aren't available (e.g., running outside
// WebChat), it degrades gracefully with manual instructions.
// -----------------------------------------------------------------------
var state = 'armed'; // armed | killing | confirmed | error
// Check if OpenClaw WebChat hooks are available
function hasWebChatHooks() {
return (typeof window.OpenClaw !== 'undefined' &&
typeof window.OpenClaw.exec === 'function');
}
// Execute the kill via OpenClaw's WebChat API
function executeKill() {
return new Promise(function(resolve, reject) {
if (!hasWebChatHooks()) {
reject(new Error('WebChat hooks not available'));
return;
}
// Call kill.sh through OpenClaw's exec interface
// REPLACE_THIS: Adjust the path if your skill is installed elsewhere
window.OpenClaw.exec({
command: 'skills/deadclaw/scripts/kill.sh',
args: ['--trigger-source', 'webchat', '--trigger-method', 'button'],
timeout: 30000
}).then(function(result) {
resolve(result);
}).catch(function(err) {
reject(err);
});
});
}
// Main click handler
function handleKill() {
if (state === 'killing' || state === 'confirmed') return;
var button = document.getElementById('killButton');
var statusText = document.getElementById('statusText');
var statusDot = document.getElementById('statusDot');
// Show killing state
state = 'killing';
button.disabled = true;
button.textContent = 'Killing...';
statusText.textContent = 'Sending kill signal...';
statusText.className = 'status-text alert';
executeKill().then(function(result) {
// Success — show confirmation with timestamp and kill count
state = 'confirmed';
button.disabled = true;
button.className = 'kill-button confirmed';
// Parse the result for timestamp and process count
var output = (result && result.output) ? result.output : '';
var killTimestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
var processCount = '?';
// Try to extract process count from kill.sh output (format: "X processes killed")
var countMatch = output.match(/(\d+)\s+processes?\s+killed/i);
if (countMatch) {
processCount = countMatch[1];
}
button.textContent = 'Stopped — ' + processCount + ' killed at ' + killTimestamp;
var summary = output || 'Kill complete. ' + processCount + ' processes killed.';
statusText.textContent = summary;
statusText.className = 'status-text success';
statusDot.className = 'status-dot killed';
// Reset after 30 seconds so the button can be used again
setTimeout(function() {
resetButton();
}, 30000);
}).catch(function(err) {
// WebChat hooks unavailable — show manual instructions
state = 'error';
button.disabled = false;
button.className = 'kill-button error';
button.textContent = 'Manual Kill Required';
statusText.innerHTML =
'WebChat hooks unavailable. Run manually: ' +
'<code style="color:#ff6600">./scripts/kill.sh</code> ' +
'or send "kill" to any connected channel.';
statusText.className = 'status-text alert';
});
}
// Reset button to armed state
function resetButton() {
state = 'armed';
var button = document.getElementById('killButton');
var statusText = document.getElementById('statusText');
var statusDot = document.getElementById('statusDot');
button.disabled = false;
button.className = 'kill-button';
button.textContent = 'Kill All Agents';
statusText.textContent = 'DeadClaw armed';
statusText.className = 'status-text';
statusDot.className = 'status-dot active';
}
// On load, check if hooks are available and update status accordingly
(function init() {
if (!hasWebChatHooks()) {
var statusText = document.getElementById('statusText');
statusText.textContent = 'DeadClaw armed (standalone mode — WebChat hooks not detected)';
}
})();
</script>
</body>
</html>
FILE:docs/android-widget-guide.md
# DeadClaw — Android Home Screen Setup Guide
This guide walks you through creating a big red DeadClaw button on your Android home screen. When you tap it, it sends the kill command to your OpenClaw Telegram bot, which stops all your running agents instantly.
**Time required**: About 5 minutes.
**What you need before starting**:
- An Android phone running Android 8.0 or later
- The Telegram app installed and logged in
- Your OpenClaw Telegram bot set up and connected
- Your Telegram bot token and chat ID
Two options are covered below. Choose whichever you prefer:
- **Option A: Tasker** — More powerful, costs a few dollars, recommended if you already have it
- **Option B: HTTP Shortcuts app** — Free, simpler, recommended for most people
---
## Option A: Using Tasker
Tasker is a powerful automation app for Android. If you already have it, this is straightforward. If you don't, Option B (HTTP Shortcuts) is free and easier.
### A1: Install Tasker
If you don't already have it, install **Tasker** from the Google Play Store. It costs about $3.50.
### A2: Create a New Task
1. Open Tasker
2. Tap the **Tasks** tab at the top
3. Tap the **+** button at the bottom
4. Name it **DeadClaw**
5. Tap the checkmark
### A3: Add an HTTP Request Action
1. In the task editor, tap the **+** button to add an action
2. Tap **Net**
3. Tap **HTTP Request**
4. Fill in:
- **Method**: POST
- **URL**: `https://api.telegram.org/botYOUR_BOT_TOKEN_HERE/sendMessage`
(Replace `YOUR_BOT_TOKEN_HERE` with your actual bot token)
- **Content Type**: `application/json`
- **Body**: `{"chat_id": "YOUR_CHAT_ID_HERE", "text": "deadclaw"}`
(Replace `YOUR_CHAT_ID_HERE` with your numeric chat ID)
5. Tap the back arrow to save
<!-- SCREENSHOT_PLACEHOLDER: tasker-http-request.png -->
### A4: Test the Task
1. Tap the **Play** button (triangle) at the bottom of the task editor
2. Check Telegram — you should see "deadclaw" arrive and DeadClaw's confirmation response
### A5: Add a Home Screen Widget
1. Go to your Android home screen
2. Long-press on an empty spot
3. Tap **Widgets**
4. Find **Tasker** in the widget list
5. Tap and hold **Task Shortcut** (1x1) and drag it to your home screen
6. Select your **DeadClaw** task
7. For the icon:
- Tap the icon to change it
- Choose a red circle or stop icon
- (Or use any icon that says "emergency" to you)
8. Tap the checkmark to confirm
<!-- SCREENSHOT_PLACEHOLDER: tasker-widget-homescreen.png -->
Tap the widget to test. Done.
---
## Option B: Using HTTP Shortcuts (Free)
HTTP Shortcuts is a free app that does exactly what we need — sends an HTTP request when you tap a home screen shortcut. No scripting knowledge needed.
### B1: Install HTTP Shortcuts
Open the Google Play Store, search for **HTTP Shortcuts** (by Roland Meyer), and install it. It's free.
<!-- SCREENSHOT_PLACEHOLDER: http-shortcuts-playstore.png -->
### B2: Create a New Shortcut
1. Open HTTP Shortcuts
2. Tap the **+** button (floating action button)
3. Tap **Regular Shortcut**
### B3: Configure the Basics
1. **Name**: DeadClaw
2. **Description**: Emergency kill switch for OpenClaw agents
3. **Method**: POST
4. **URL**: `https://api.telegram.org/botYOUR_BOT_TOKEN_HERE/sendMessage`
Replace `YOUR_BOT_TOKEN_HERE` with your actual Telegram bot token. You got this from BotFather when you created your bot. It looks like `7123456789:AAHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`.
<!-- SCREENSHOT_PLACEHOLDER: http-shortcuts-basic.png -->
### B4: Set Up the Request Body
1. Tap **Request Body / Parameters**
2. Change the body type to **Custom Text**
3. Set Content Type to **application/json**
4. In the body text field, enter:
```json
{"chat_id": "YOUR_CHAT_ID_HERE", "text": "deadclaw"}
```
Replace `YOUR_CHAT_ID_HERE` with your numeric Telegram chat ID.
<!-- SCREENSHOT_PLACEHOLDER: http-shortcuts-body.png -->
### B5: Set the Icon
1. Go back to the main shortcut settings
2. Tap the **Icon** field
3. Choose a built-in icon — look for a circle or stop symbol
4. Tap the **Color** option and choose **red**
<!-- SCREENSHOT_PLACEHOLDER: http-shortcuts-icon.png -->
### B6: Set the Response Handling
1. Tap **Response Handling**
2. Under **Success Message**, choose **Show simple toast** — this shows a brief confirmation when the kill fires
3. Tap save/back
### B7: Test It
1. Back in the HTTP Shortcuts main screen, tap on your **DeadClaw** shortcut
2. It should fire the request and show a toast message
3. Check Telegram — "deadclaw" should appear and your bot should confirm the kill
### B8: Add to Home Screen
1. Long-press on the **DeadClaw** shortcut in the HTTP Shortcuts app
2. Tap **Place on home screen** (or **Add to home screen**)
3. The DeadClaw button appears on your home screen
Alternatively:
1. Go to your home screen
2. Long-press an empty area
3. Tap **Widgets**
4. Find **HTTP Shortcuts** in the widget list
5. Drag the 1x1 widget to your home screen
6. Select your **DeadClaw** shortcut
<!-- SCREENSHOT_PLACEHOLDER: http-shortcuts-homescreen.png -->
---
## Finding Your Telegram Bot Token and Chat ID
If you're not sure where to find these, here's how:
**Bot token:**
1. Open Telegram
2. Search for **@BotFather**
3. If you already have a bot, send `/mybots`, select your bot, then tap **API Token**
4. If you need a new bot, send `/newbot` and follow the prompts
5. Copy the token (looks like `7123456789:AAHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`)
**Chat ID:**
1. Open Telegram
2. Search for **@userinfobot** and start a chat with it
3. It will reply with your chat ID (a number like `123456789`)
4. If you're using a group chat, add @userinfobot to the group — it will post the group's chat ID (starts with `-100`)
---
## Troubleshooting
**"Nothing happens when I tap the button"**
Check that your phone has an internet connection. Then verify your bot token — one wrong character and it won't work.
**"I see the request was sent but Telegram doesn't respond"**
Make sure your OpenClaw instance is running and connected to the Telegram bot. The message "deadclaw" needs to reach an active OpenClaw agent to trigger the kill.
**"The widget disappeared after a phone restart"**
Some Android launchers remove widgets on restart. Try adding it again. If it keeps disappearing, check your battery optimization settings — some phones aggressively kill background apps and widgets.
**"I want a bigger button"**
In Tasker, you can create a 2x2 or larger widget. In HTTP Shortcuts, the widget is fixed at 1x1 but you can place it in a prominent spot on your home screen.
**"Can I use this without Telegram?"**
Yes — if you have another messaging channel connected to OpenClaw (WhatsApp, Discord, Slack), you can modify the shortcut to send the kill trigger through that channel's API instead. The concept is the same: send the word "deadclaw" to a channel your OpenClaw agent is listening on.
FILE:docs/clawhub-listing.md
# ClawHub Listing — DeadClaw
## Metadata
- **Slug**: `deadclaw`
- **Install command**: `openclaw skill install deadclaw`
- **Version**: 1.0.0
- **Author**: Kintupercy
- **License**: MIT
- **Tags**: `security`, `kill-switch`, `emergency`, `agent-safety`, `watchdog`
- **Category**: Security & Safety
---
## Short Description (140 characters)
Emergency kill switch for OpenClaw agents. One message, one button, or one phone tap — everything stops. Includes auto-kill watchdog.
---
## Long Description
### DeadClaw — Emergency Kill Switch
**One tap. Everything stops.**
DeadClaw is a single-purpose emergency kill switch for OpenClaw agents. When something goes wrong — a runaway loop, suspicious behavior, unexpected token burn — DeadClaw halts all running agents instantly. No terminal required. No technical knowledge needed.
Built in response to the ClawHavoc attack (February 2026), which exposed 1,184 malicious skills in the OpenClaw ecosystem. If you run agents autonomously, you need a reliable way to stop everything from wherever you are.
**Three activation methods:**
- **Message trigger** — Send "kill" to any connected channel (Telegram, WhatsApp, Discord, Slack). Works from your phone, anywhere in the world.
- **WebChat button** — A persistent red kill button in your OpenClaw dashboard. One click.
- **Phone shortcut** — A home screen button on iOS or Android. One tap from your lock screen.
All three methods execute the same kill sequence: terminate all agent processes, back up and pause all cron jobs, kill all active sessions, and write a detailed incident log.
**Watchdog (automatic protection):**
DeadClaw includes a background monitor that auto-kills agents when it detects: runaway loops (30min+), excessive token spend (50k tokens in 10min), unauthorized network calls, or file writes outside your workspace. All thresholds are configurable.
**After the kill:**
Send "status" to see what's running. Send "restore" to bring agents back online from the most recent backup. Every script supports --dry-run for safe testing.
**Key features:**
- Idempotent kill script — safe to trigger twice
- Crontab backup before every clear
- Cross-platform (macOS + Linux)
- Works across Telegram, WhatsApp, Discord, Slack
- Step-by-step phone setup guides for non-technical users
- Incident logging with full process details
---
## Screenshots
<!-- REPLACE_THIS: Add screenshots of the WebChat button and phone home screen shortcut -->
1. `screenshot-webchat-button.png` — The red kill button in the WebChat dashboard
2. `screenshot-iphone-homescreen.png` — DeadClaw shortcut on an iPhone home screen
3. `screenshot-kill-confirmation.png` — Telegram confirmation after a kill
4. `screenshot-status-report.png` — Status report in a messaging channel
---
## Install
```bash
openclaw skill install deadclaw
```
---
## Requirements
- OpenClaw v2.0+
- Bash 4.0+
- At least one connected messaging channel (Telegram, WhatsApp, Discord, or Slack) for message triggers
- Optional: OpenClaw WebChat for the dashboard button
- Optional: iOS Shortcuts or Android Tasker/HTTP Shortcuts for the phone shortcut
FILE:docs/competitive-notes.md
# DeadClaw — Competitive Notes (Internal)
This document is for internal reference. It compares DeadClaw to existing security tools in the OpenClaw ecosystem and outlines our key differentiators.
---
## Landscape
There are two established security tools for OpenClaw agents:
### openclaw-defender
A comprehensive security suite that includes agent sandboxing, permission management, network filtering, process isolation, and audit logging. It's the most fully-featured security tool in the ecosystem.
**Strengths**: Deep integration with OpenClaw internals, granular permission controls, sandboxing layer that prevents malicious actions before they happen, active community.
**Weaknesses**: Requires significant technical knowledge to configure. Setup involves editing YAML config files, setting up permission rules, configuring sandbox policies. Minimum viable setup takes 30+ minutes for a developer, longer for non-technical users. No mobile activation. Terminal-only interface.
### clawsec
A security monitoring and response tool focused on real-time threat detection. Uses heuristic analysis to identify suspicious agent behavior, maintains a threat signature database, and can auto-respond to known attack patterns.
**Strengths**: Sophisticated detection engine, regularly updated threat signatures (especially after ClawHavoc), detailed threat analysis reports, integration with popular alerting tools.
**Weaknesses**: Heavy resource footprint — runs multiple background services. Complex configuration required to tune detection sensitivity (too sensitive = false positives, too loose = missed threats). No simple kill mechanism — response actions are configured through rule files. Developer-only tool. No mobile interface.
---
## DeadClaw's Position
DeadClaw is not competing with these tools on breadth of features. It occupies a different niche entirely.
### Key Differentiators
**1. Single purpose vs. security suite**
openclaw-defender and clawsec are Swiss Army knives. DeadClaw is an emergency stop button. It does one thing and does it reliably. There are no configuration files to tune, no rule engines to maintain, no permission policies to write. Install it and it works.
This matters because when you're panicking at 2am because your agents are doing something unexpected, you don't want to remember which config file controls which response action. You want a button that says "stop."
**2. Phone-first activation vs. terminal-only**
Neither openclaw-defender nor clawsec can be activated from a phone. Both require terminal access. DeadClaw was designed from the ground up for phone activation — message triggers work from any messaging app, and the home screen shortcut puts a physical kill button on your phone.
This is the feature that's genuinely new in the ecosystem. Nobody has built this.
**3. Non-technical users vs. developer-only**
openclaw-defender's README assumes you know what YAML is. clawsec's setup guide includes `pip install` and `systemctl` commands. DeadClaw's phone setup guides are written for people who have never used a terminal. The iPhone guide walks through every single tap in iOS Shortcuts.
This matters because the OpenClaw user base is growing beyond developers. People are running agents for personal productivity, small business operations, content creation. They need security tools that don't require a CS degree.
**4. Instant setup vs. complex configuration**
DeadClaw: `openclaw skill install deadclaw`. Message triggers work immediately. Phone shortcut takes 5 minutes. Total time to full protection: under 10 minutes.
openclaw-defender: Install, create config directory, write sandbox policy, configure permissions, test policies, iterate. Realistic setup time: 1-2 hours for someone who knows what they're doing.
clawsec: Install, configure threat detection rules, tune sensitivity, set up alerting integrations, test with dry runs. Realistic setup time: 1-3 hours.
**5. Complements, doesn't replace**
DeadClaw works alongside defender and clawsec. If you have defender's sandboxing preventing most attacks and clawsec's detection catching threats, DeadClaw is your last-resort emergency stop when something gets through both layers. It's the fire alarm, not the fire suppression system.
---
## Competitive Risks
**Risk: defender or clawsec adds a kill switch feature.**
Likely response to DeadClaw's success. But a kill switch bolted onto a complex tool still requires that complex tool's setup. DeadClaw's value is that it's standalone and dead simple. As long as we stay focused on that, a "me too" feature in a larger suite won't match the experience.
**Risk: "just one feature" perception.**
Some people will dismiss DeadClaw as trivial — "it's just a script that runs pkill." The value isn't in the script complexity. The value is in the activation surface (phone, any messaging app, dashboard button) and the audience (non-technical users). The competitive moat is simplicity and accessibility, not technical sophistication.
**Risk: OpenClaw adds native kill functionality.**
If OpenClaw builds emergency stop into the core platform, DeadClaw becomes less necessary. Monitor OpenClaw's roadmap. If this happens, DeadClaw pivots to being the best mobile interface for that native functionality.
---
## Messaging Guidance
When talking about DeadClaw publicly:
- Never disparage defender or clawsec. They're good tools solving different problems.
- Position DeadClaw as complementary, not competitive.
- Lead with the use case (phone kill, non-technical users), not feature comparisons.
- The ClawHavoc story is the hook — it establishes urgency and credibility.
- The phone home screen shortcut is the demo that gets attention. Always show it.
FILE:docs/iphone-shortcut-guide.md
# DeadClaw — iPhone Home Screen Setup Guide
This guide walks you through creating a big red DeadClaw button on your iPhone home screen. When you tap it, it sends the kill command to your OpenClaw Telegram bot, which stops all your running agents instantly.
**Time required**: About 5 minutes.
**What you need before starting**:
- An iPhone running iOS 15 or later
- The Telegram app installed and logged in
- Your OpenClaw Telegram bot set up and connected (you should already be able to send messages to it)
- Your Telegram bot's chat ID or group chat ID
If you don't have a Telegram bot connected to OpenClaw yet, set that up first using the OpenClaw docs, then come back here.
---
## Step 1: Open the Shortcuts App
Find the **Shortcuts** app on your iPhone. It has a blue and pink icon that looks like overlapping squares. It comes pre-installed on every iPhone.
If you can't find it, swipe down on your home screen to open search, then type "Shortcuts" and tap it.
<!-- SCREENSHOT_PLACEHOLDER: shortcuts-app-icon.png -->
---
## Step 2: Create a New Shortcut
Tap the **+** button in the top-right corner. This opens a new, blank shortcut.
<!-- SCREENSHOT_PLACEHOLDER: new-shortcut-screen.png -->
---
## Step 3: Add the "Get Contents of URL" Action
This is the action that sends the kill message to your Telegram bot.
1. Tap **Add Action** (or tap the search bar at the bottom)
2. In the search field, type **URL**
3. Tap **Get Contents of URL** from the results
<!-- SCREENSHOT_PLACEHOLDER: search-get-contents-url.png -->
---
## Step 4: Configure the Telegram API Call
Now you need to fill in the details so the shortcut sends a message to your Telegram bot.
1. Tap the **URL** field and enter:
```
https://api.telegram.org/botYOUR_BOT_TOKEN_HERE/sendMessage
```
**REPLACE** `YOUR_BOT_TOKEN_HERE` with your actual Telegram bot token. It looks something like `7123456789:AAHxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx`. You got this when you created your bot with BotFather.
2. Tap **Show More** (or the arrow next to the URL)
3. Change **Method** to **POST**
4. Under **Request Body**, tap **JSON**
5. Add two fields:
- Key: `chat_id` — Value: your chat ID (a number like `123456789` or `-100123456789` for a group)
- Key: `text` — Value: `deadclaw`
To add each field:
- Tap **Add new field**
- Choose **Text** as the type
- Enter the key name and value
<!-- SCREENSHOT_PLACEHOLDER: configure-url-action.png -->
---
## Step 5: Test It
Before adding it to your home screen, test it.
1. Tap the **Play** button (triangle) in the bottom-right corner
2. The shortcut will run and send "deadclaw" to your Telegram bot
3. Check Telegram — you should see the message arrive and DeadClaw's confirmation response
If it doesn't work, double-check your bot token and chat ID. The most common issue is a typo in the bot token.
<!-- SCREENSHOT_PLACEHOLDER: test-run-result.png -->
---
## Step 6: Name Your Shortcut
1. Tap the dropdown arrow at the top of the screen (next to the default name)
2. Tap **Rename**
3. Type **DeadClaw**
<!-- SCREENSHOT_PLACEHOLDER: rename-shortcut.png -->
---
## Step 7: Set the Icon
Give it a red icon so it's immediately recognizable on your home screen.
1. Tap the dropdown arrow at the top again
2. Tap **Choose Icon**
3. Tap the **Color** circle and choose **Red**
4. For the glyph/symbol, search for and select the **stop** icon (a square), or **xmark.circle** (an X in a circle) — whichever feels more like an emergency button to you
<!-- SCREENSHOT_PLACEHOLDER: set-icon-red.png -->
---
## Step 8: Add to Home Screen
This is the step that puts the button on your home screen.
1. Tap the dropdown arrow at the top one more time
2. Tap **Add to Home Screen**
3. You'll see a preview of how it will look
4. Tap **Add** in the top-right corner
The DeadClaw button now appears on your home screen like any other app.
<!-- SCREENSHOT_PLACEHOLDER: add-to-homescreen.png -->
---
## Step 9: Move It Where You Want It
Long-press the DeadClaw icon and drag it wherever you want. Some suggestions:
- **Your main home screen** — so it's always one tap away
- **The dock** — if you want maximum accessibility
- **A "Tools" folder** — if you prefer a tidier home screen but still want quick access
---
## Step 10: Test From the Home Screen
Tap the DeadClaw button on your home screen. It should:
1. Flash briefly as the shortcut runs
2. Send "deadclaw" to your Telegram bot
3. Your bot should respond with the kill confirmation
That's it. You now have a one-tap emergency kill switch on your phone.
---
## Optional: Make It Work From the Lock Screen
If you want even faster access, you can add the shortcut to your Lock Screen (iOS 16+):
1. Long-press your Lock Screen
2. Tap **Customize**
3. Tap the Lock Screen
4. Tap one of the bottom widget slots
5. Choose **Shortcuts** and select your DeadClaw shortcut
Now you can trigger a kill without even unlocking your phone.
---
## Troubleshooting
**"The shortcut ran but nothing happened in Telegram"**
Check your bot token. Go to Telegram, find your chat with @BotFather, and verify the token matches what you entered in the shortcut.
**"I get an error about the chat_id"**
Make sure you're using the numeric chat ID, not a username. You can get your chat ID by messaging @userinfobot on Telegram.
**"The shortcut asks for permission every time"**
Go to Settings > Shortcuts > Advanced, and turn on **Allow Running Scripts**. Also make sure **Private Sharing** is enabled.
**"I want to change the trigger word"**
Edit the shortcut (long-press the icon, tap Edit Shortcut) and change the `text` value in the JSON body to whatever trigger word you prefer.
FILE:docs/launch-post.md
# DeadClaw Launch Post — OpenClaw Community (Discord)
---
**Posting to: #announcements or #new-skills**
---
Two weeks ago, the ClawHavoc attack hit the OpenClaw ecosystem. 1,184 malicious skills. Some of them sat quietly in people's environments for weeks before anyone noticed. The community response has been great — the security audit, the new review process, the detection tools.
But there's a gap nobody's filled yet.
When you discover something wrong at 2am on your phone, what do you actually do? SSH into your server? Open a terminal? If you're running agents overnight — and a lot of us are — there's no fast, non-technical way to just stop everything.
That's what DeadClaw does. One action, everything stops.
**Three ways to trigger it:**
**Message trigger.** Send "kill" to any connected OpenClaw channel — Telegram, WhatsApp, Discord, Slack. From your phone, from a friend's phone, from anywhere. DeadClaw detects it, kills all running agents, pauses all cron jobs (after backing up your crontab), and confirms back to that same channel. This is the feature nobody else has built yet.
**WebChat button.** A persistent red kill button in your WebChat dashboard. For when something is going wrong right in front of you. One click.
**Phone home screen shortcut.** A big red button on your phone's home screen. One tap sends the kill trigger to Telegram. Included are step-by-step setup guides for both iOS Shortcuts and Android (Tasker/HTTP Shortcuts). Written for non-technical users, takes under 5 minutes.
All three methods run the exact same kill script.
**There's also a watchdog.** A lightweight background monitor that auto-kills agents if it detects: runaway loops (30min+), excessive token burn (50k tokens in 10min), outbound network calls to domains not on your whitelist, or file writes outside your workspace. You get an alert explaining exactly why it fired. Configurable thresholds.
**After a kill:** send "status" for a health report, send "restore" to bring everything back from the backup. Every script supports `--dry-run` for safe testing.
DeadClaw is not a security suite. It's not trying to replace openclaw-defender or clawsec. It does one thing. It does it from your phone. It works for people who don't use terminals.
Install:
```
openclaw skill install deadclaw
```
GitHub: https://github.com/Kintupercy/deadclaw
If this is useful to you, a star on the repo helps other people find it. If something's broken or missing, open an issue — happy to fix it.
Stay safe out there.
FILE:docs/roadmap.md
# DeadClaw — Roadmap
---
## v1.0 — Emergency Kill Switch (Current Release)
Everything in this release is about one thing: stopping all agents instantly from anywhere.
**Core kill functionality**:
- Kill all running OpenClaw agent processes (SIGTERM then SIGKILL)
- Back up and clear OpenClaw cron jobs
- Terminate all active agent sessions
- Write timestamped incident log
- Confirm back to triggering channel
**Three activation methods**:
- Message trigger via any connected channel (Telegram, WhatsApp, Discord, Slack)
- WebChat dashboard kill button (HTML widget)
- Phone home screen shortcut (iOS Shortcuts + Android Tasker/HTTP Shortcuts)
**Watchdog (passive protection)**:
- Background daemon monitoring on 60-second intervals
- Auto-kill on: runaway loops (30min+), token burn (50k/10min), unauthorized network calls, sandbox escape
- Configurable thresholds via environment variables
- PID file management for clean start/stop
**Recovery**:
- Status command for health reports
- Restore command with confirmation step
- Crontab backup before every clear
**Developer experience**:
- --dry-run flag on all scripts
- Idempotent kill (safe to trigger twice)
- Cross-platform support (macOS + Linux)
- Clear comments throughout all scripts
---
## v2.0 — Proactive Breach Detection
v2 shifts from reactive (stop things after they go wrong) to proactive (detect threats before a kill is needed). The watchdog evolves from a threshold monitor into an intelligent threat detection system.
**Planned features**:
- **Behavioral analysis**: Learn normal agent patterns (typical runtime, token spend curves, network call frequency) and alert on deviations. Rather than fixed thresholds, detect when agents are behaving abnormally for their specific workload.
- **Real-time alerts without killing**: New alert tier between "everything's fine" and "kill everything." Warnings sent to your phone when an agent approaches a threshold, giving you time to investigate before the watchdog auto-kills. Configurable alert channels separate from kill triggers.
- **Skill reputation scoring**: Cross-reference installed skills against ClawHub's community reports and known-malicious skill signatures. Flag agents running skills with low reputation scores or recently reported vulnerabilities.
- **Network traffic analysis**: Move beyond domain whitelisting to actual traffic pattern analysis. Detect data exfiltration attempts (large outbound payloads), command-and-control communication patterns, and encrypted connections to unusual endpoints.
- **File integrity monitoring**: Track which files agents modify and flag unexpected changes to system files, credentials, SSH keys, or other sensitive paths — even within the allowed workspace.
- **Incident replay**: Record agent activity (process state snapshots, network logs, file changes) so that after a kill, you can replay exactly what the agent was doing. Helps answer "what happened?" not just "something happened."
**Architecture changes**:
- Watchdog rewritten as a lightweight Go binary for better performance and cross-platform consistency
- Persistent state store for behavioral baselines (SQLite)
- Webhook support for alerts (in addition to messaging channels)
- REST API for programmatic access to status and alert data
---
## v3.0 — Companion Mobile App
v3 brings DeadClaw to a native mobile experience. Instead of shortcut workarounds and Telegram bots, a purpose-built app with push notifications and a real-time dashboard.
**Planned features**:
- **Native iOS and Android app**: Purpose-built DeadClaw app with a prominent kill button, agent activity dashboard, and alert history.
- **Push notifications**: Real-time push alerts when the watchdog detects a threat or auto-triggers a kill. No dependency on Telegram or other messaging apps.
- **Agent activity dashboard**: See all running agents, their uptime, token spend, and network activity in real time. Visual indicators for agent health (green/yellow/red).
- **One-tap kill from notification**: When you receive a threat alert, kill all agents directly from the notification without opening the app.
- **Kill history and analytics**: Review past kill events, see patterns (which agents cause the most kills, what times of day, which threat types), and get recommendations for hardening your setup.
- **Multi-environment support**: Monitor and kill agents across multiple machines/servers from a single app. Add environments by scanning a QR code or entering a connection token.
- **Apple Watch / Wear OS complication**: A kill button on your watch face. Tap your wrist, agents stop.
- **Biometric confirmation**: Optional Face ID / fingerprint confirmation before a kill fires, for environments where accidental triggers would be costly.
**Architecture changes**:
- DeadClaw agent component runs alongside the watchdog on each monitored machine
- Secure WebSocket connection between agent and mobile app (end-to-end encrypted)
- Cloud relay service for push notifications (optional — direct connection mode available for privacy-conscious users)
- API gateway for multi-environment management
---
## Beyond v3 — Ideas Under Consideration
These aren't committed to any version. They're ideas we're exploring based on community feedback.
- **Team mode**: Multiple users can kill the same environment. Useful for teams running shared agent infrastructure. Kill events show who triggered them.
- **Scheduled safe hours**: Define time windows when agents are allowed to run autonomously. Outside those windows, the watchdog is extra aggressive.
- **Integration marketplace**: Pre-built integrations with popular monitoring tools (Grafana, Datadog, PagerDuty) for teams that want DeadClaw alerts in their existing dashboards.
- **Agent quarantine**: Instead of killing an agent immediately, isolate it — cut network access and freeze file writes — so you can investigate while it's still running.
- **Community threat feed**: Anonymized, opt-in sharing of watchdog trigger events across DeadClaw users to build a collective threat detection model.
FILE:README.md
# DeadClaw
**One tap. Everything stops.**
DeadClaw is an emergency kill switch for OpenClaw agents. When something goes wrong — a runaway loop, suspicious behavior, or you just need everything stopped right now — DeadClaw halts all running agents instantly from wherever you are. Your phone, your browser, any messaging app. One action, everything stops.
Works with both **native OpenClaw installs** and **Docker-based deployments** (Hostinger VPS, etc.). Auto-detects your setup.
---
## Quick Start
```bash
# Install
git clone https://github.com/Kintupercy/deadclaw.git
cd deadclaw
chmod +x scripts/*.sh
# Test (safe — doesn't kill anything)
bash scripts/kill.sh --dry-run
# Check what's running
bash scripts/status.sh
# Kill everything (for real)
bash scripts/kill.sh
# Bring everything back
bash scripts/restore.sh
```
---
## Why DeadClaw Exists
In February 2026, the ClawHavoc attack exposed 1,184 malicious skills in the OpenClaw ecosystem. If you're running agents autonomously — especially overnight or unattended — you need a fast, reliable way to shut everything down from anywhere.
DeadClaw does one thing: stops everything. Designed for anyone to set up in under five minutes and activate from their phone lock screen.
---
## The Four Scripts
### `kill.sh` — The Panic Button
Shuts everything down immediately:
- Kills all OpenClaw agent processes (SIGTERM, then SIGKILL for anything stubborn)
- Stops all OpenClaw Docker containers (kills sessions inside first, then stops)
- Backs up your crontab to a timestamped file, then removes OpenClaw cron entries
- Pauses launchd agents (macOS) or systemd services (Linux)
- Logs everything to `deadclaw.log`
- Sends confirmation back to the triggering channel
```bash
bash scripts/kill.sh # Kill everything
bash scripts/kill.sh --dry-run # See what WOULD happen without killing
```
### `status.sh` — The Dashboard
Shows what's running. Read-only, safe to run anytime.
```bash
bash scripts/status.sh # Human-readable report
bash scripts/status.sh --json # Machine-readable output
```
Example output:
```
DeadClaw Status Report
======================
Docker container: openclaw-573j-openclaw-1
Status: running
Started: 2026-02-21T18:54:39Z
OpenClaw status:
Agents: 1 total, 1 sessions
Channels: Telegram OK
Containers: 2 running
Host processes: 2 running
- openclaw-gatewa (PID 104620) — up 5d 3h
- openclaw-gatewa (PID 137072) — up 3d 17h
Watchdog: Not running
```
### `restore.sh` — The Undo Button
After a kill, brings everything back:
1. Shows you exactly what will be restored (crontab entries, containers)
2. Waits for your explicit confirmation (`yes`/`no`)
3. Restores crontab from the most recent backup
4. Restarts stopped Docker containers
5. Detects the OpenClaw gateway
```bash
bash scripts/restore.sh # Interactive restore
bash scripts/restore.sh --dry-run # Preview only
```
The watchdog does NOT auto-start after restore. You verify stability first, then start it manually when ready.
### `watchdog.sh` — The Automated Guard
Runs in the background, checks every 60 seconds:
- **Runaway loops** — Any agent running longer than 30 minutes
- **Token burn** — More than 50,000 tokens spent in under 10 minutes
- **Unauthorized network calls** — Connections to domains not on your whitelist
- **Sandbox escape** — File writes outside your designated workspace
If any threshold is exceeded, it auto-triggers `kill.sh`.
```bash
bash scripts/watchdog.sh start # Start monitoring
bash scripts/watchdog.sh start --dry-run # Monitor and log, but don't auto-kill
bash scripts/watchdog.sh status # Check if running
bash scripts/watchdog.sh stop # Stop monitoring
```
The watchdog waits 5 minutes after starting before its first check (grace period), so it won't false-trigger on sessions that were just restored.
---
## Three Ways to Activate
### 1. Message Trigger
Send any of these words to any connected OpenClaw channel (Telegram, WhatsApp, Discord, Slack):
- `kill` or `KILL`
- `dead`
- `stop everything`
- `emergency stop`
- `deadclaw`
### 2. WebChat Kill Button
A persistent red button in your OpenClaw WebChat dashboard. One click stops everything.
```javascript
OpenClaw.WebChat.registerWidget('deadclaw-button', {
src: 'skills/deadclaw/ui/deadclaw-button.html',
position: 'top-bar',
persistent: true
});
```
### 3. Phone Home Screen Shortcut
A big red button on your phone's home screen. One tap sends the kill trigger.
- [iPhone setup guide](docs/iphone-shortcut-guide.md) (iOS Shortcuts, 5 minutes)
- [Android setup guide](docs/android-widget-guide.md) (Tasker or HTTP Shortcuts, 5 minutes)
---
## Configuration
All thresholds are configurable via environment variables. Set them before starting the watchdog:
```bash
export DEADCLAW_MAX_RUNTIME_MIN=45 # default: 30
export DEADCLAW_MAX_TOKENS=100000 # default: 50000
export DEADCLAW_TOKEN_WINDOW_MIN=15 # default: 10
export DEADCLAW_WHITELIST=./my-whitelist.txt # one domain per line
export DEADCLAW_WORKSPACE=/path/to/workspace
```
| Variable | Default | What It Controls |
|---|---|---|
| `DEADCLAW_MAX_RUNTIME_MIN` | 30 | Max agent runtime before auto-kill |
| `DEADCLAW_MAX_TOKENS` | 50000 | Max token spend in the monitoring window |
| `DEADCLAW_TOKEN_WINDOW_MIN` | 10 | Token spend monitoring window (minutes) |
| `DEADCLAW_WHITELIST` | `./network-whitelist.txt` | Allowed outbound domains file |
| `DEADCLAW_WORKSPACE` | `$OPENCLAW_WORKSPACE` | Designated workspace directory |
| `OPENCLAW_PROCESS_PATTERN` | _(none)_ | Additional process name pattern to match |
### Network Whitelist
Create `network-whitelist.txt` with one allowed domain per line:
```
api.openai.com
api.anthropic.com
github.com
# Add your own domains here
```
The watchdog kills agents that make outbound calls to any domain not in this list.
---
## Platform Support
| Feature | Linux VPS | macOS (Mac Mini) |
|---------|-----------|------------------|
| Process detection | works | works |
| Docker containers | works | works |
| Crontab backup/restore | works | works |
| Scheduled tasks | systemd | launchd |
| Process uptime | native | fallback parser |
| Watchdog monitoring | works | works |
Scripts auto-detect the OS and use the right commands.
---
## FAQ
**Is DeadClaw safe to trigger accidentally?**
Yes. The kill is idempotent — triggering it when no agents are running does nothing harmful. Your crontab is always backed up before any changes, and you can restore everything with `restore.sh`.
**Will it kill non-OpenClaw processes?**
No. DeadClaw only targets processes matching OpenClaw agent patterns and Docker containers named `openclaw*`.
**Does the watchdog use a lot of resources? Does it burn tokens?**
No and no. The watchdog uses only system commands (`pgrep`, `ps`, `docker exec openclaw status`, `lsof`) — all local operations that read process state and files on disk. Zero API calls, zero AI tokens. The entire cost is one lightweight bash process sleeping 60 seconds between checks.
**Can I customize the trigger words?**
Yes. Edit the `trigger_keywords` list in [SKILL.md](SKILL.md).
**What if I'm offline when the watchdog triggers?**
The kill still executes locally. The alert is sent when connectivity is available. Everything is logged to `deadclaw.log` regardless.
**Does it work with Docker?**
Yes. DeadClaw auto-detects Docker containers named `openclaw*`, kills sessions inside them via `docker exec`, then stops the containers. Restore brings them back with `docker start`.
---
## Files
```
deadclaw/
SKILL.md — OpenClaw skill definition
README.md — This file
deadclaw.log — Incident log (auto-created on first use)
backups/ — Crontab backups (auto-created)
scripts/
kill.sh — Core kill script
watchdog.sh — Background monitor daemon
status.sh — Health report
restore.sh — Post-kill recovery
ui/
deadclaw-button.html — WebChat kill button widget
docs/
clawhub-listing.md — ClawHub product listing
launch-post.md — Community announcement
iphone-shortcut-guide.md — iOS Shortcuts setup guide
android-widget-guide.md — Android widget setup guide
competitive-notes.md — Competitive analysis
roadmap.md — v1-v3 roadmap
```
---
## License
MIT
---
## Links
- **ClawHub**: `openclaw skill install deadclaw`
- **GitHub**: https://github.com/Kintupercy/deadclaw
- **Issues**: https://github.com/Kintupercy/deadclaw/issues
FILE:scripts/kill.sh
#!/usr/bin/env bash
# ============================================================================
# DeadClaw — kill.sh
# Core emergency kill script for OpenClaw agents.
#
# What this script does:
# 1. Finds and kills all running OpenClaw agent processes (native + Docker)
# 2. Backs up the current crontab, then clears all OpenClaw cron jobs
# 3. Kills all active OpenClaw agent sessions
# 4. Writes a timestamped incident log to deadclaw.log
# 5. Sends a confirmation message back to the triggering channel
#
# Supports both:
# - Native OpenClaw installs (processes on the host)
# - Docker-based OpenClaw installs (Hostinger VPS, etc.)
#
# Usage:
# ./kill.sh # Execute the kill
# ./kill.sh --dry-run # Log what would happen, don't kill anything
# ./kill.sh --trigger-source slack # Specify the trigger source for logging
# ./kill.sh --trigger-method message # Specify the trigger method for logging
#
# This script is idempotent — running it twice is safe and won't cause errors.
# ============================================================================
set -euo pipefail
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
# Directory where this script lives (used to find deadclaw.log and other scripts)
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
LOG_FILE="SKILL_DIR/deadclaw.log"
BACKUP_DIR="SKILL_DIR/backups"
# Defaults for logging context
DRY_RUN=false
TRIGGER_SOURCE="-unknown"
TRIGGER_METHOD="-manual"
# ---------------------------------------------------------------------------
# Parse command-line arguments
# ---------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
shift
;;
--trigger-source)
if [[ $# -lt 2 ]]; then
echo "Error: --trigger-source requires a value"
exit 1
fi
# Sanitize: strip newlines/control chars to prevent log injection
TRIGGER_SOURCE=$(echo "$2" | tr -d '\n\r' | head -c 200)
shift 2
;;
--trigger-method)
if [[ $# -lt 2 ]]; then
echo "Error: --trigger-method requires a value"
exit 1
fi
TRIGGER_METHOD=$(echo "$2" | tr -d '\n\r' | head -c 200)
shift 2
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
# ---------------------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------------------
# Timestamp in ISO 8601 format
timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
# Log a message to both stderr (for display) and the incident log file.
# Using stderr so log messages don't interfere with function return values
# captured via $(...) subshells.
log_event() {
local msg="$1"
echo "[$(timestamp)] $msg" >&2
echo "[$(timestamp)] $msg" >> "$LOG_FILE"
}
# Detect the operating system so we can use the right process management commands
detect_os() {
case "$(uname -s)" in
Darwin*) echo "macos" ;;
Linux*) echo "linux" ;;
*) echo "unknown" ;;
esac
}
# ---------------------------------------------------------------------------
# Docker support — detect and manage OpenClaw Docker containers
# ---------------------------------------------------------------------------
# Find running Docker containers that match OpenClaw patterns
find_openclaw_containers() {
if ! command -v docker &>/dev/null; then
return
fi
docker ps --filter "name=openclaw" --format "{{.Names}}" 2>/dev/null || true
}
# Kill sessions and stop Docker containers
kill_docker() {
local containers
containers=$(find_openclaw_containers)
local count=0
if [[ -z "$containers" ]]; then
log_event "DOCKER: No OpenClaw Docker containers found."
echo "0"
return
fi
while IFS= read -r container; do
[[ -n "$container" ]] || continue
if [[ "$DRY_RUN" == true ]]; then
local status
status=$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || echo "unknown")
log_event "DRY-RUN: Would stop Docker container: $container (status: $status)"
else
# Graceful: kill sessions inside the container first
docker exec "$container" openclaw session kill-all &>/dev/null || true
log_event "DOCKER: Killed sessions in container: $container"
# Stop the container (10s grace period for clean shutdown)
docker stop -t 10 "$container" &>/dev/null || true
log_event "DOCKER: Stopped container: $container"
fi
count=$((count + 1))
done <<< "$containers"
log_event "DOCKER: count OpenClaw containers stopped."
echo "$count"
}
# Send a confirmation message back to the triggering channel.
# Uses OpenClaw's messaging hooks if available, falls back to stdout.
# Tries Docker exec if openclaw isn't available on the host.
send_confirmation() {
local message="$1"
# Try OpenClaw's messaging API on the host first
if command -v openclaw &>/dev/null; then
openclaw message send \
--channel "TRIGGER_SOURCE" \
--text "message" 2>/dev/null || true
else
# Try via a running Docker container
local container
container=$(find_openclaw_containers | head -1)
if [[ -n "$container" ]]; then
docker exec "$container" openclaw message send \
--channel "TRIGGER_SOURCE" \
--text "message" 2>/dev/null || true
fi
fi
# Always print to stdout as a fallback
echo "$message"
}
# ---------------------------------------------------------------------------
# Step 1: Find all running OpenClaw agent processes
# ---------------------------------------------------------------------------
find_openclaw_processes() {
# Look for processes matching common OpenClaw agent patterns.
# We search for multiple patterns to catch agents started different ways.
local pids=()
# Pattern 1: Processes with "openclaw" in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "openclaw[-_ ]agent" 2>/dev/null || true)
# Pattern 2: Processes with "claw-agent" in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "claw-agent" 2>/dev/null || true)
# Pattern 3: Processes with "openclaw-skill" in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "openclaw-skill" 2>/dev/null || true)
# Pattern 4: Processes with "clawdbot" in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "clawdbot" 2>/dev/null || true)
# Pattern 5: Processes with "moltbot" in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "moltbot" 2>/dev/null || true)
# Pattern 6: Processes with OpenClaw gateway in the command line
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "openclaw.*gateway" 2>/dev/null || true)
# Pattern 7: Processes matching the OPENCLAW_PROCESS_PATTERN env var
# (allows users to add custom patterns for their specific setup)
# Safety: reject overly broad patterns that could match everything
if [[ -n "-" ]]; then
if [[ "OPENCLAW_PROCESS_PATTERN" == ".*" || "OPENCLAW_PROCESS_PATTERN" == "*" || "OPENCLAW_PROCESS_PATTERN" == "." || #OPENCLAW_PROCESS_PATTERN -lt 3 ]]; then
log_event "WARNING: OPENCLAW_PROCESS_PATTERN='OPENCLAW_PROCESS_PATTERN' is too broad — ignored for safety. Use a specific pattern (3+ chars)."
else
while IFS= read -r pid; do
[[ -n "$pid" ]] && pids+=("$pid")
done < <(pgrep -f "OPENCLAW_PROCESS_PATTERN" 2>/dev/null || true)
fi
fi
# Deduplicate PIDs (guard against empty array)
if [[ #pids[@] -eq 0 ]]; then
return
fi
printf '%s\n' "pids[@]" | sort -u
}
# ---------------------------------------------------------------------------
# Step 2: Kill all OpenClaw processes
# ---------------------------------------------------------------------------
kill_processes() {
local pids
pids=$(find_openclaw_processes)
local count=0
local killed_pids=()
if [[ -z "$pids" ]]; then
log_event "KILL: No OpenClaw agent processes found."
echo "0"
return
fi
while IFS= read -r pid; do
if [[ "$DRY_RUN" == true ]]; then
log_event "DRY-RUN: Would kill PID $pid ($(ps -p "$pid" -o comm= 2>/dev/null || echo 'unknown'))"
else
# Send SIGTERM first (graceful), then SIGKILL after 5 seconds if still alive
if kill -TERM "$pid" 2>/dev/null; then
killed_pids+=("$pid")
count=$((count + 1))
fi
fi
done <<< "$pids"
# If not a dry run, wait briefly then force-kill any survivors
if [[ "$DRY_RUN" == false && #killed_pids[@] -gt 0 ]]; then
sleep 2
for pid in "killed_pids[@]"; do
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null || true
log_event "KILL: Force-killed stubborn process PID $pid"
fi
done
fi
log_event "KILL: count OpenClaw processes terminated. PIDs: -none"
echo "$count"
}
# ---------------------------------------------------------------------------
# Step 3: Back up and clear OpenClaw cron jobs
# ---------------------------------------------------------------------------
pause_cron_jobs() {
local os
os=$(detect_os)
local cron_count=0
# Create backup directory if it doesn't exist
mkdir -p "$BACKUP_DIR"
# Back up the current crontab before touching anything
local backup_file="BACKUP_DIR/deadclaw-crontab-backup-$(date +%Y%m%d-%H%M%S).txt"
if crontab -l &>/dev/null; then
crontab -l > "$backup_file" 2>/dev/null || true
log_event "CRON: Crontab backed up to backup_file"
# Count OpenClaw-related cron entries
# Note: grep -c outputs "0" with exit code 1 when no matches found.
# Using `|| true` avoids capturing a second "0" from a fallback echo.
cron_count=$(grep -c -i "openclaw\|claw-agent\|clawdbot\|moltbot" "$backup_file" 2>/dev/null || true)
cron_count=-0
if [[ "$DRY_RUN" == true ]]; then
log_event "DRY-RUN: Would remove cron_count OpenClaw cron entries"
else
# Surgical removal: only OpenClaw-related entries, preserve everything else
crontab -l 2>/dev/null | grep -v -i "openclaw\|claw-agent\|clawdbot\|moltbot" | crontab - 2>/dev/null || true
log_event "CRON: cron_count OpenClaw cron entries removed"
fi
else
log_event "CRON: No crontab found. Nothing to back up or clear."
fi
# Handle OS-specific scheduled tasks
if [[ "$os" == "macos" ]]; then
# Pause any OpenClaw launchd agents
local agents_paused=0
for plist in ~/Library/LaunchAgents/com.openclaw.*; do
[[ -f "$plist" ]] || continue
if [[ "$DRY_RUN" == true ]]; then
log_event "DRY-RUN: Would unload launchd agent: $(basename "$plist")"
else
launchctl unload "$plist" 2>/dev/null || true
log_event "CRON: Unloaded launchd agent: $(basename "$plist")"
fi
agents_paused=$((agents_paused + 1))
done
cron_count=$((cron_count + agents_paused))
elif [[ "$os" == "linux" ]]; then
# Pause any OpenClaw systemd user services
local services_paused=0
while IFS= read -r service; do
[[ -n "$service" ]] || continue
if [[ "$DRY_RUN" == true ]]; then
log_event "DRY-RUN: Would stop systemd service: $service"
else
systemctl --user stop "$service" 2>/dev/null || true
systemctl --user disable "$service" 2>/dev/null || true
log_event "CRON: Stopped systemd service: $service"
fi
services_paused=$((services_paused + 1))
done < <(systemctl --user list-units --type=service --no-legend 2>/dev/null | grep -i "openclaw\|claw-agent\|clawdbot\|moltbot" | awk '{print $1}' || true)
cron_count=$((cron_count + services_paused))
fi
echo "$cron_count"
}
# ---------------------------------------------------------------------------
# Step 4: Kill active OpenClaw sessions
# ---------------------------------------------------------------------------
kill_sessions() {
# If OpenClaw CLI is available on host, use it to terminate active sessions
if command -v openclaw &>/dev/null; then
if [[ "$DRY_RUN" == true ]]; then
local session_list
session_list=$(openclaw session list --format json 2>/dev/null || echo "[]")
log_event "DRY-RUN: Would terminate all active OpenClaw sessions: session_list"
else
openclaw session kill-all 2>/dev/null || true
log_event "SESSIONS: All active OpenClaw sessions terminated"
fi
else
# Docker sessions are already killed in kill_docker() before container stop.
# If there were no Docker containers either, note it.
local containers
containers=$(find_openclaw_containers)
if [[ -z "$containers" ]]; then
log_event "SESSIONS: OpenClaw CLI not found and no Docker containers — skipping session cleanup"
else
log_event "SESSIONS: Sessions killed via Docker (handled in container stop sequence)"
fi
fi
}
# ---------------------------------------------------------------------------
# Step 5: Get current token spend (for the incident log)
# ---------------------------------------------------------------------------
get_token_spend() {
# Try to read token spend from OpenClaw's metrics if available
if command -v openclaw &>/dev/null; then
openclaw metrics token-spend --format plain 2>/dev/null || echo "unknown"
else
# Try via Docker container
local container
container=$(find_openclaw_containers | head -1)
if [[ -n "$container" ]]; then
docker exec "$container" openclaw metrics token-spend --format plain 2>/dev/null || echo "unknown"
elif [[ -f "-/tmp/.openclaw/metrics/tokens.log" ]]; then
tail -1 "-/tmp/.openclaw/metrics/tokens.log" 2>/dev/null || echo "unknown"
else
echo "unknown"
fi
fi
}
# ---------------------------------------------------------------------------
# Main execution
# ---------------------------------------------------------------------------
main() {
local ts
ts=$(timestamp)
echo ""
echo "============================================"
if [[ "$DRY_RUN" == true ]]; then
echo " DeadClaw — DRY RUN (no actions taken)"
else
echo " DeadClaw — Emergency Kill Activated"
fi
echo " ts"
echo "============================================"
echo ""
# Write incident log header
log_event "=========================================="
log_event "DEADCLAW KILL EVENT"
log_event "Trigger source: TRIGGER_SOURCE"
log_event "Trigger method: TRIGGER_METHOD"
log_event "Dry run: DRY_RUN"
log_event "=========================================="
# Execute the kill sequence — both native processes and Docker containers
local processes_killed
processes_killed=$(kill_processes)
local containers_killed
containers_killed=$(kill_docker)
local cron_paused
cron_paused=$(pause_cron_jobs)
kill_sessions
local token_spend
token_spend=$(get_token_spend)
# Combine native + Docker counts for the summary
local total_killed=$((processes_killed + containers_killed))
# Write summary to incident log
log_event "SUMMARY: processes_killed processes killed, containers_killed containers stopped, cron_paused cron jobs paused, token spend: token_spend"
log_event "=========================================="
# Build and send the confirmation message
local prefix=""
[[ "$DRY_RUN" == true ]] && prefix="[DRY-RUN] "
local confirmation
confirmation="prefix🔴 DeadClaw activated. All agents stopped. ts — total_killed killed (processes_killed processes, containers_killed containers). cron_paused cron jobs paused. See deadclaw.log for full report."
send_confirmation "$confirmation"
# Stop the watchdog too (if it's running), since all agents are dead
if [[ "$DRY_RUN" == false ]]; then
"SCRIPT_DIR/watchdog.sh" stop 2>/dev/null || true
fi
}
main "$@"
exit 0
FILE:scripts/restore.sh
#!/usr/bin/env bash
# ============================================================================
# DeadClaw — restore.sh
# Post-kill recovery script. Restores the backed-up crontab and optionally
# restarts previously running agents.
#
# What this script does:
# 1. Shows what will be restored (crontab entries, agent list)
# 2. Waits for explicit confirmation before proceeding
# 3. Restores the most recent crontab backup
# 4. Restarts the watchdog (optional)
# 5. Sends a confirmation message
#
# Usage:
# ./restore.sh # Interactive restore with confirmation prompt
# ./restore.sh --dry-run # Show what would be restored, don't do anything
# ./restore.sh --confirm # Skip the confirmation prompt (for automation)
#
# Safety: This script never restarts anything without confirmation. After a
# kill event, you want to be sure before bringing things back online.
# ============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
LOG_FILE="SKILL_DIR/deadclaw.log"
BACKUP_DIR="SKILL_DIR/backups"
DRY_RUN=false
AUTO_CONFIRM=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true; shift ;;
--confirm) AUTO_CONFIRM=true; shift ;;
*) shift ;;
esac
done
timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
log_event() {
local msg="$1"
echo "[$(timestamp)] RESTORE: $msg" >> "$LOG_FILE"
}
send_message() {
local message="$1"
if command -v openclaw &>/dev/null; then
openclaw message broadcast --text "message" 2>/dev/null || true
else
# Try via Docker container
local container
container=$(find_openclaw_containers | head -1)
if [[ -n "$container" ]]; then
docker exec "$container" openclaw message broadcast --text "message" 2>/dev/null || true
fi
fi
echo "$message"
}
# Find running OpenClaw Docker containers
find_openclaw_containers() {
if ! command -v docker &>/dev/null; then
return
fi
docker ps --filter "name=openclaw" --format "{{.Names}}" 2>/dev/null || true
}
# Find stopped OpenClaw Docker containers
find_stopped_containers() {
if ! command -v docker &>/dev/null; then
return
fi
docker ps -a --filter "name=openclaw" --filter "status=exited" --format "{{.Names}}" 2>/dev/null || true
}
# ---------------------------------------------------------------------------
# Step 1: Find the most recent crontab backup
# ---------------------------------------------------------------------------
find_latest_backup() {
if [[ ! -d "$BACKUP_DIR" ]]; then
echo ""
return
fi
# Find the newest backup file
ls -t "BACKUP_DIR"/deadclaw-crontab-backup-*.txt 2>/dev/null | head -1
}
# ---------------------------------------------------------------------------
# Step 2: Show what will be restored
# ---------------------------------------------------------------------------
show_restore_plan() {
local backup_file="$1"
echo ""
echo "============================================"
echo " DeadClaw — Restore Plan"
echo "============================================"
echo ""
if [[ -n "$backup_file" && -f "$backup_file" ]]; then
local entry_count
entry_count=$(wc -l < "$backup_file" | tr -d ' ')
echo "Crontab backup found: $(basename "$backup_file")"
echo " Contains entry_count entries:"
echo ""
# Show the crontab entries (indent them for readability)
while IFS= read -r line; do
echo " line"
done < "$backup_file"
echo ""
else
echo "No crontab backup found. Nothing to restore."
echo ""
fi
# Check for any OpenClaw launchd/systemd services that were disabled
local os
os=$(uname -s)
if [[ "$os" == "Darwin" ]]; then
local disabled_agents
disabled_agents=$(find ~/Library/LaunchAgents -name "com.openclaw.*" 2>/dev/null || true)
if [[ -n "$disabled_agents" ]]; then
echo "LaunchAgents to re-enable:"
echo "$disabled_agents" | while read -r f; do echo " $(basename "$f")"; done
echo ""
fi
elif [[ "$os" == "Linux" ]]; then
local disabled_services
disabled_services=$(systemctl --user list-unit-files --type=service 2>/dev/null | grep -i "openclaw\|claw-agent\|clawdbot\|moltbot" | grep "disabled" || true)
if [[ -n "$disabled_services" ]]; then
echo "Systemd services to re-enable:"
echo "$disabled_services" | while read -r line; do echo " $line"; done
echo ""
fi
fi
# Check for stopped Docker containers
local stopped_containers
stopped_containers=$(find_stopped_containers)
if [[ -n "$stopped_containers" ]]; then
echo "Docker containers to restart:"
while IFS= read -r c; do
[[ -n "$c" ]] && echo " $c"
done <<< "$stopped_containers"
echo ""
fi
echo "Note: The watchdog will NOT auto-start. Start it manually after verifying stability."
echo ""
}
# ---------------------------------------------------------------------------
# Step 3: Wait for confirmation
# ---------------------------------------------------------------------------
get_confirmation() {
local backup_file="$1"
if [[ "$AUTO_CONFIRM" == true ]]; then
return 0
fi
if [[ "$DRY_RUN" == true ]]; then
echo "[DRY-RUN] Would wait for confirmation here. Skipping."
return 1 # Don't proceed in dry-run
fi
# Build a specific prompt showing what will be restored
local prompt_detail="Restore"
if [[ -n "$backup_file" && -f "$backup_file" ]]; then
local job_count
job_count=$(wc -l < "$backup_file" | tr -d ' ')
# Extract timestamp from filename: deadclaw-crontab-backup-YYYYMMDD-HHMMSS.txt
local backup_ts
backup_ts=$(basename "$backup_file" | sed 's/deadclaw-crontab-backup-//;s/\.txt//')
prompt_detail="Restore job_count cron jobs from backup taken at backup_ts"
fi
echo "============================================"
echo " prompt_detail? (yes/no)"
echo "============================================"
echo ""
read -r response
case "$response" in
confirm|yes|YES|y|Y)
return 0
;;
*)
echo "Restore cancelled."
log_event "Restore cancelled by user."
return 1
;;
esac
}
# ---------------------------------------------------------------------------
# Step 4: Execute the restore
# ---------------------------------------------------------------------------
do_restore() {
local backup_file="$1"
local restored_items=0
log_event "Restore initiated."
# Restore crontab
if [[ -n "$backup_file" && -f "$backup_file" ]]; then
crontab "$backup_file" 2>/dev/null
if [[ $? -eq 0 ]]; then
log_event "Crontab restored from backup_file"
echo "Crontab restored."
restored_items=$((restored_items + 1))
else
log_event "Failed to restore crontab from backup_file"
echo "Warning: Failed to restore crontab."
fi
fi
# Re-enable OS-specific services
local os
os=$(uname -s)
if [[ "$os" == "Darwin" ]]; then
for plist in ~/Library/LaunchAgents/com.openclaw.*; do
[[ -f "$plist" ]] || continue
launchctl load "$plist" 2>/dev/null || true
log_event "Re-enabled launchd agent: $(basename "$plist")"
restored_items=$((restored_items + 1))
done
elif [[ "$os" == "Linux" ]]; then
while IFS= read -r service; do
[[ -n "$service" ]] || continue
local svc_name
svc_name=$(echo "$service" | awk '{print $1}')
systemctl --user enable "$svc_name" 2>/dev/null || true
systemctl --user start "$svc_name" 2>/dev/null || true
log_event "Re-enabled systemd service: svc_name"
restored_items=$((restored_items + 1))
done < <(systemctl --user list-unit-files --type=service 2>/dev/null | grep -i "openclaw\|claw-agent\|clawdbot\|moltbot" | grep "disabled" || true)
fi
# Restart stopped Docker containers
local stopped_containers
stopped_containers=$(find_stopped_containers)
if [[ -n "$stopped_containers" ]]; then
while IFS= read -r container; do
[[ -n "$container" ]] || continue
docker start "$container" &>/dev/null || true
log_event "Restarted Docker container: container"
echo "Docker container restarted: container"
restored_items=$((restored_items + 1))
done <<< "$stopped_containers"
# Wait for containers to be ready
echo "Waiting for containers to initialize..."
sleep 5
fi
# Attempt to restart the OpenClaw gateway process
if command -v openclaw &>/dev/null; then
if openclaw gateway start 2>/dev/null; then
log_event "OpenClaw gateway restarted."
echo "OpenClaw gateway restarted."
restored_items=$((restored_items + 1))
else
log_event "Failed to restart OpenClaw gateway (may need manual start)."
echo "Warning: Could not restart OpenClaw gateway. Start it manually."
fi
else
# Try via Docker container (check running containers now)
local running_container
running_container=$(find_openclaw_containers | head -1)
if [[ -n "$running_container" ]]; then
log_event "OpenClaw gateway running inside Docker container: running_container"
echo "OpenClaw gateway running inside Docker container: running_container"
else
log_event "OpenClaw CLI not found and no Docker containers — skipping gateway restart."
echo "Note: OpenClaw CLI not found. Start the gateway manually."
fi
fi
log_event "Restore complete. restored_items items restored."
# Send confirmation — don't auto-start the watchdog. After a kill event,
# the user should verify things are stable before enabling auto-kill.
local msg="DeadClaw restore complete. restored_items items restored. All systems nominal. Start watchdog manually: ./scripts/watchdog.sh start"
send_message "$msg"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
local backup_file
backup_file=$(find_latest_backup)
show_restore_plan "$backup_file"
if [[ "$DRY_RUN" == true ]]; then
echo "[DRY-RUN] No changes made."
exit 0
fi
if get_confirmation "$backup_file"; then
do_restore "$backup_file"
fi
}
main "$@"
FILE:scripts/status.sh
#!/usr/bin/env bash
# ============================================================================
# DeadClaw — status.sh
# Health report script. Sends a plain-English summary of what's running,
# how long it's been running, current token spend rate, and watchdog status.
#
# Supports both native and Docker-based OpenClaw installs.
#
# Usage:
# ./status.sh # Print status to stdout
# ./status.sh --dry-run # Same behavior (status is read-only)
# ./status.sh --json # Output as JSON for programmatic use
#
# Trigger: user sends "status" to any connected OpenClaw channel.
# ============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
PID_FILE="SKILL_DIR/deadclaw-watchdog.pid"
OUTPUT_JSON=false
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
--json) OUTPUT_JSON=true; shift ;;
--dry-run) shift ;; # status is already read-only
*) shift ;;
esac
done
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
# Cross-platform: get elapsed time in seconds for a PID.
get_elapsed_seconds() {
local pid="$1"
local seconds
seconds=$(ps -o etimes= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ -n "$seconds" && "$seconds" =~ ^[0-9]+$ ]]; then
echo "$seconds"
return
fi
local etime
etime=$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ -z "$etime" ]]; then
echo "0"
return
fi
local days=0 hours=0 mins=0 secs=0
if [[ "$etime" == *-* ]]; then
days="etime%%-*"
etime="etime#*-"
fi
IFS=: read -ra parts <<< "$etime"
case #parts[@] in
3) hours="parts[0]"; mins="parts[1]"; secs="parts[2]" ;;
2) mins="parts[0]"; secs="parts[1]" ;;
1) secs="parts[0]" ;;
esac
days=$((10#$days)) hours=$((10#$hours)) mins=$((10#$mins)) secs=$((10#$secs))
echo $(( days*86400 + hours*3600 + mins*60 + secs ))
}
# Format seconds into human-readable uptime
format_uptime() {
local elapsed="$1"
if [[ "$elapsed" -ge 86400 ]]; then
echo "$((elapsed / 86400))d $((elapsed % 86400 / 3600))h"
elif [[ "$elapsed" -ge 3600 ]]; then
echo "$((elapsed / 3600))h $((elapsed % 3600 / 60))m"
elif [[ "$elapsed" -ge 60 ]]; then
echo "$((elapsed / 60))m $((elapsed % 60))s"
else
echo "elapseds"
fi
}
# All OpenClaw process patterns in one place
OPENCLAW_PGREP_PATTERN="openclaw[-_ ]agent|claw-agent|openclaw-skill|clawdbot|moltbot|openclaw.*gateway"
# Find running OpenClaw Docker containers
find_openclaw_containers() {
if ! command -v docker &>/dev/null; then
return
fi
docker ps --filter "name=openclaw" --format "{{.Names}}" 2>/dev/null || true
}
# ---------------------------------------------------------------------------
# Gather information — Docker mode
# ---------------------------------------------------------------------------
gather_docker_status() {
local containers
containers=$(find_openclaw_containers)
if [[ -z "$containers" ]]; then
return 1
fi
# List containers with uptime
echo ""
echo "DeadClaw Status Report"
echo "======================"
echo ""
local container_count=0
while IFS= read -r container; do
[[ -n "$container" ]] || continue
container_count=$((container_count + 1))
local status uptime_str
status=$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || echo "unknown")
uptime_str=$(docker inspect --format '{{.State.StartedAt}}' "$container" 2>/dev/null || echo "unknown")
echo "Docker container: container"
echo " Status: status"
echo " Started: uptime_str"
echo ""
# Get detailed status from inside the container
local oc_status
oc_status=$(docker exec "$container" openclaw status --all 2>/dev/null || echo "(could not reach openclaw inside container)")
echo " OpenClaw status:"
echo "$oc_status" | while IFS= read -r line; do
echo " $line"
done
echo ""
done <<< "$containers"
echo "Containers: container_count running"
echo ""
# Also check for native processes on the host
local native_pids
native_pids=$(pgrep -f "OPENCLAW_PGREP_PATTERN" 2>/dev/null || true)
if [[ -n "$native_pids" ]]; then
local native_count
native_count=$(echo "$native_pids" | wc -l | tr -d ' ')
echo "Host processes: native_count running"
while IFS= read -r pid; do
[[ -n "$pid" ]] || continue
local pname elapsed uptime
pname=$(ps -o comm= -p "$pid" 2>/dev/null || echo "unknown")
elapsed=$(get_elapsed_seconds "$pid")
uptime=$(format_uptime "$elapsed")
echo " - pname (PID pid) — up uptime"
done <<< "$native_pids"
echo ""
fi
# Watchdog status
if [[ -f "$PID_FILE" ]]; then
local wpid
wpid=$(cat "$PID_FILE" 2>/dev/null)
if kill -0 "$wpid" 2>/dev/null; then
echo "Watchdog: Active (PID wpid)"
else
echo "Watchdog: Not running (stale PID file)"
fi
else
echo "Watchdog: Not running"
fi
echo ""
return 0
}
# ---------------------------------------------------------------------------
# Gather information — Native mode (no Docker)
# ---------------------------------------------------------------------------
gather_native_status() {
declare -a agent_names=()
declare -a agent_pids=()
declare -a agent_uptimes=()
while IFS= read -r pid; do
[[ -n "$pid" ]] || continue
agent_pids+=("$pid")
local_name=$(ps -o comm= -p "$pid" 2>/dev/null || echo "unknown")
agent_names+=("$local_name")
local_elapsed=$(get_elapsed_seconds "$pid")
agent_uptimes+=("$(format_uptime "$local_elapsed")")
done < <(pgrep -f "OPENCLAW_PGREP_PATTERN" 2>/dev/null || true)
local agent_count=#agent_pids[@]
# Token spend rate
local token_rate="unknown"
local token_total="unknown"
local token_log="-/tmp/.openclaw/metrics/tokens.log"
if [[ -f "$token_log" ]]; then
local_total=$(tail -50 "$token_log" 2>/dev/null | awk '{sum += $2} END {print sum+0}' || echo "0")
token_total="$local_total"
token_rate="~local_total tokens/10min"
fi
# Watchdog status
local watchdog_running=false
local watchdog_pid=""
if [[ -f "$PID_FILE" ]]; then
watchdog_pid=$(cat "$PID_FILE" 2>/dev/null)
if kill -0 "$watchdog_pid" 2>/dev/null; then
watchdog_running=true
fi
fi
# Warnings
local warnings=()
local max_runtime=-30
local max_tokens=-50000
for i in "!agent_pids[@]"; do
local_elapsed=$(get_elapsed_seconds "agent_pids[$i]")
local_threshold_sec=$((max_runtime * 60))
local_warning_sec=$((local_threshold_sec * 80 / 100))
if [[ "$local_elapsed" -ge "$local_warning_sec" ]]; then
warnings+=("Agent agent_names[$i] (PID agent_pids[$i]) has been running agent_uptimes[$i] — approaching max_runtimemin kill threshold")
fi
done
if [[ "$token_total" != "unknown" && "$token_total" -ge $((max_tokens * 80 / 100)) ]]; then
warnings+=("Token spend is at token_total — approaching max_tokens kill threshold")
fi
if [[ "$OUTPUT_JSON" == true ]]; then
local agents_json="["
for i in "!agent_pids[@]"; do
[[ $i -gt 0 ]] && agents_json+=","
agents_json+="{\"name\":\"agent_names[$i]\",\"pid\":agent_pids[$i],\"uptime\":\"agent_uptimes[$i]\"}"
done
agents_json+="]"
local warnings_json="["
for i in "!warnings[@]"; do
[[ $i -gt 0 ]] && warnings_json+=","
warnings_json+="\"warnings[$i]\""
done
warnings_json+="]"
cat <<EOF
{
"agents_running": agent_count,
"agents": agents_json,
"token_rate": "token_rate",
"watchdog_active": watchdog_running,
"watchdog_pid": "watchdog_pid",
"warnings": warnings_json
}
EOF
else
echo ""
echo "DeadClaw Status Report"
echo "======================"
echo ""
if [[ "$agent_count" -eq 0 ]]; then
echo "Agents: None running."
else
echo "Agents: agent_count running"
for i in "!agent_pids[@]"; do
echo " - agent_names[$i] (PID agent_pids[$i]) — up agent_uptimes[$i]"
done
fi
echo ""
echo "Token spend: token_rate"
echo ""
if [[ "$watchdog_running" == true ]]; then
echo "Watchdog: Active (PID watchdog_pid)"
else
echo "Watchdog: Not running"
fi
if [[ #warnings[@] -gt 0 ]]; then
echo ""
echo "Warnings:"
for w in "warnings[@]"; do
echo " ! w"
done
fi
echo ""
fi
}
# ---------------------------------------------------------------------------
# Main — detect Docker vs native and gather status
# ---------------------------------------------------------------------------
containers=$(find_openclaw_containers)
if [[ -n "$containers" ]]; then
gather_docker_status
else
gather_native_status
fi
FILE:scripts/watchdog.sh
#!/usr/bin/env bash
# ============================================================================
# DeadClaw — watchdog.sh
# Background monitor daemon that auto-triggers the kill script when it detects
# dangerous conditions in running OpenClaw agents.
#
# What this script monitors (every 60 seconds):
# 1. Runaway loops — any agent running longer than 30 minutes
# 2. Token burn — token spend exceeding 50,000 in under 10 minutes
# 3. Unauthorized network calls — outbound connections to non-whitelisted domains
# 4. Sandbox escape — file writes outside the designated workspace
#
# Usage:
# ./watchdog.sh start # Start the watchdog daemon
# ./watchdog.sh stop # Stop the watchdog daemon
# ./watchdog.sh status # Check if watchdog is running
# ./watchdog.sh start --dry-run # Monitor and log but don't auto-kill
#
# The watchdog writes a PID file so it can be stopped and restarted cleanly.
# ============================================================================
set -uo pipefail
# ---------------------------------------------------------------------------
# Configuration — all overridable via environment variables
# ---------------------------------------------------------------------------
SCRIPT_DIR="$(cd "$(dirname "BASH_SOURCE[0]")" && pwd)"
SKILL_DIR="$(dirname "$SCRIPT_DIR")"
LOG_FILE="SKILL_DIR/deadclaw.log"
PID_FILE="SKILL_DIR/deadclaw-watchdog.pid"
# Thresholds (with sensible defaults)
MAX_RUNTIME_MIN="-30"
MAX_TOKENS="-50000"
TOKEN_WINDOW_MIN="-10"
WHITELIST_FILE="-${SKILL_DIR/network-whitelist.txt}"
WORKSPACE="-${OPENCLAW_WORKSPACE:-}"
CHECK_INTERVAL=60 # seconds between checks
GRACE_PERIOD=300 # seconds to wait before first check (lets sessions settle after restart)
# Validate numeric thresholds — fall back to defaults if non-numeric
[[ "$MAX_RUNTIME_MIN" =~ ^[0-9]+$ ]] || { echo "Warning: DEADCLAW_MAX_RUNTIME_MIN='MAX_RUNTIME_MIN' is not a number. Using default 30." >&2; MAX_RUNTIME_MIN=30; }
[[ "$MAX_TOKENS" =~ ^[0-9]+$ ]] || { echo "Warning: DEADCLAW_MAX_TOKENS='MAX_TOKENS' is not a number. Using default 50000." >&2; MAX_TOKENS=50000; }
[[ "$TOKEN_WINDOW_MIN" =~ ^[0-9]+$ ]] || { echo "Warning: DEADCLAW_TOKEN_WINDOW_MIN='TOKEN_WINDOW_MIN' is not a number. Using default 10." >&2; TOKEN_WINDOW_MIN=10; }
DRY_RUN=false
# ---------------------------------------------------------------------------
# Parse command-line arguments
# ---------------------------------------------------------------------------
ACTION="-"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
shift
;;
*)
shift
;;
esac
done
# ---------------------------------------------------------------------------
# Helper functions
# ---------------------------------------------------------------------------
timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
# Compact log line for watchdog checks (keeps the log small)
wlog() {
echo "[$(timestamp)] WATCHDOG: $1" >> "$LOG_FILE"
}
# Cross-platform helper: get elapsed time in seconds for a PID.
# Linux has ps -o etimes= (seconds directly). macOS only has ps -o etime=
# which returns [[DD-]HH:]MM:SS format that we need to parse.
get_elapsed_seconds() {
local pid="$1"
# Try Linux-style etimes first (returns seconds directly)
local seconds
seconds=$(ps -o etimes= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ -n "$seconds" && "$seconds" =~ ^[0-9]+$ ]]; then
echo "$seconds"
return
fi
# Fall back to etime format: [[DD-]HH:]MM:SS
local etime
etime=$(ps -o etime= -p "$pid" 2>/dev/null | tr -d ' ')
if [[ -z "$etime" ]]; then
echo "0"
return
fi
local days=0 hours=0 mins=0 secs=0
# Split off days if present (format: DD-HH:MM:SS)
if [[ "$etime" == *-* ]]; then
days="etime%%-*"
etime="etime#*-"
fi
# Split remaining HH:MM:SS or MM:SS or SS
IFS=: read -ra parts <<< "$etime"
case #parts[@] in
3) hours="parts[0]"; mins="parts[1]"; secs="parts[2]" ;;
2) mins="parts[0]"; secs="parts[1]" ;;
1) secs="parts[0]" ;;
esac
# Strip leading zeros to avoid octal interpretation
days=$((10#$days)) hours=$((10#$hours)) mins=$((10#$mins)) secs=$((10#$secs))
echo $(( days*86400 + hours*3600 + mins*60 + secs ))
}
# All OpenClaw process patterns in one place
OPENCLAW_PGREP_PATTERN="openclaw[-_ ]agent|claw-agent|openclaw-skill|clawdbot|moltbot|openclaw.*gateway"
# Find running OpenClaw Docker containers
find_openclaw_containers() {
if ! command -v docker &>/dev/null; then
return
fi
docker ps --filter "name=openclaw" --format "{{.Names}}" 2>/dev/null || true
}
# Trigger the kill script with a reason
trigger_kill() {
local reason="$1"
wlog "AUTO-TRIGGER: reason"
local dry_flag=""
[[ "$DRY_RUN" == true ]] && dry_flag="--dry-run"
"SCRIPT_DIR/kill.sh" \
--trigger-source "watchdog" \
--trigger-method "auto" \
$dry_flag
# Send the specific watchdog alert message
local prefix=""
[[ "$DRY_RUN" == true ]] && prefix="[DRY-RUN] "
local alert="prefix🔴 DeadClaw auto-triggered. Reason: reason. All processes stopped. Check deadclaw.log."
if command -v openclaw &>/dev/null; then
openclaw message broadcast --text "alert" 2>/dev/null || true
fi
echo "$alert"
}
# ---------------------------------------------------------------------------
# Check 1: Runaway loops — agents running longer than the max threshold
# ---------------------------------------------------------------------------
check_runtime() {
local max_seconds=$((MAX_RUNTIME_MIN * 60))
# Check native OpenClaw agent processes
while IFS= read -r pid; do
[[ -n "$pid" ]] || continue
local elapsed
elapsed=$(get_elapsed_seconds "$pid")
if [[ "$elapsed" -gt "$max_seconds" ]]; then
local cmd
cmd=$(ps -o comm= -p "$pid" 2>/dev/null || echo "unknown")
local runtime_min=$((elapsed / 60))
trigger_kill "agent loop exceeded MAX_RUNTIME_MINmin threshold (PID pid, cmd, running for runtime_minmin)"
return 1
fi
done < <(pgrep -f "OPENCLAW_PGREP_PATTERN" 2>/dev/null || true)
# Check Docker container sessions
local containers
containers=$(find_openclaw_containers)
if [[ -n "$containers" ]]; then
while IFS= read -r container; do
[[ -n "$container" ]] || continue
# Get session ages from inside the container via openclaw status
# Parse the sessions table for age values like "27m ago", "2h ago"
local session_info
session_info=$(docker exec "$container" openclaw status 2>/dev/null || true)
# Extract session age lines — look for patterns like "27m ago" or "2h ago"
local ages
ages=$(echo "$session_info" | grep -oE '[0-9]+[hm] +ago' 2>/dev/null || true)
while IFS= read -r age_str; do
[[ -n "$age_str" ]] || continue
local age_min=0
if [[ "$age_str" =~ ^([0-9]+)h ]]; then
age_min=$(( BASH_REMATCH[1] * 60 ))
elif [[ "$age_str" =~ ^([0-9]+)m ]]; then
age_min=BASH_REMATCH[1]
fi
if [[ "$age_min" -gt "$MAX_RUNTIME_MIN" ]]; then
trigger_kill "agent session in container container exceeded MAX_RUNTIME_MINmin threshold (running for age_minmin)"
return 1
fi
done <<< "$ages"
done <<< "$containers"
fi
return 0
}
# ---------------------------------------------------------------------------
# Check 2: Token burn — excessive token spend in a short window
# ---------------------------------------------------------------------------
check_token_spend() {
local token_log="-/tmp/.openclaw/metrics/tokens.log"
# In Docker mode, try to get token info from containers
if [[ ! -f "$token_log" ]]; then
local containers
containers=$(find_openclaw_containers)
if [[ -n "$containers" ]]; then
while IFS= read -r container; do
[[ -n "$container" ]] || continue
# Try to get token spend from the container's openclaw status
local status_output
status_output=$(docker exec "$container" openclaw status 2>/dev/null || true)
# Look for token info like "0.0k/400k (0%)" in the status output
local token_match
token_match=$(echo "$status_output" | grep -oE '[0-9.]+k/[0-9.]+k *\([0-9]+%\)' 2>/dev/null | head -1 || true)
if [[ -n "$token_match" ]]; then
# Extract used tokens (the first number before /)
local used_k
used_k=$(echo "$token_match" | grep -oE '^[0-9.]+' || echo "0")
# Convert k to actual tokens (rough)
local used_tokens
used_tokens=$(echo "$used_k * 1000" | bc 2>/dev/null || echo "0")
used_tokens=used_tokens%.* # truncate decimal
if [[ "$used_tokens" -gt "$MAX_TOKENS" ]]; then
trigger_kill "token spend exceeded MAX_TOKENS tokens in container container (actual: ~used_tokens tokens)"
return 1
fi
fi
done <<< "$containers"
fi
return 0
fi
# If the token log doesn't exist and no Docker, skip silently
[[ -f "$token_log" ]] || return 0
local window_seconds=$((TOKEN_WINDOW_MIN * 60))
local cutoff
cutoff=$(date -u -d "window_seconds seconds ago" +%s 2>/dev/null || \
date -u -v-window_secondsS +%s 2>/dev/null || echo "0")
# Token log format expected: TIMESTAMP TOKENS_USED
# Sum up tokens used within the window
local total_tokens=0
while IFS=' ' read -r ts tokens; do
[[ -n "$ts" && -n "$tokens" ]] || continue
local entry_epoch
entry_epoch=$(date -u -d "$ts" +%s 2>/dev/null || date -u -jf "%Y-%m-%dT%H:%M:%SZ" "$ts" +%s 2>/dev/null || echo "0")
if [[ "$entry_epoch" -ge "$cutoff" ]]; then
total_tokens=$((total_tokens + tokens))
fi
done < "$token_log"
if [[ "$total_tokens" -gt "$MAX_TOKENS" ]]; then
trigger_kill "token spend exceeded MAX_TOKENS tokens in TOKEN_WINDOW_MINmin window (actual: total_tokens tokens)"
return 1
fi
return 0
}
# ---------------------------------------------------------------------------
# Check 3: Unauthorized network calls — connections to non-whitelisted domains
# ---------------------------------------------------------------------------
check_network() {
# Skip if no whitelist file exists (user hasn't configured network restrictions)
[[ -f "$WHITELIST_FILE" ]] || return 0
# Get current outbound connections from OpenClaw agent processes (native)
local agent_pids
agent_pids=$(pgrep -f "OPENCLAW_PGREP_PATTERN" 2>/dev/null || true)
# In Docker mode, also check container network connections
if [[ -z "$agent_pids" ]]; then
local containers
containers=$(find_openclaw_containers)
if [[ -n "$containers" ]]; then
# For Docker containers, we check from the host using docker's network
while IFS= read -r container; do
[[ -n "$container" ]] || continue
local container_pid
container_pid=$(docker inspect --format '{{.State.Pid}}' "$container" 2>/dev/null || true)
[[ -n "$container_pid" && "$container_pid" != "0" ]] && agent_pids="agent_pids+$'\n'container_pid"
done <<< "$containers"
fi
fi
[[ -n "$agent_pids" ]] || return 0
# Load whitelist into an associative array for fast lookup
declare -A allowed_domains
while IFS= read -r domain; do
domain=$(echo "$domain" | tr -d '[:space:]')
[[ -n "$domain" && "$domain" != \#* ]] && allowed_domains["$domain"]=1
done < "$WHITELIST_FILE"
# Check each agent process for outbound connections
while IFS= read -r pid; do
[[ -n "$pid" ]] || continue
# Use lsof or ss to find network connections for this PID
local connections
connections=$(lsof -i -a -p "$pid" -n 2>/dev/null | grep "ESTABLISHED" || true)
while IFS= read -r conn; do
[[ -n "$conn" ]] || continue
# Extract the remote address/hostname
local remote
remote=$(echo "$conn" | awk '{print $9}' | cut -d':' -f1 | sed 's/->/ /g' | awk '{print $NF}')
[[ -n "$remote" ]] || continue
# Try reverse DNS lookup
local hostname
hostname=$(host "$remote" 2>/dev/null | awk '/domain name pointer/ {print $NF}' | sed 's/\.$//' || echo "$remote")
# Check if this domain (or any parent domain) is whitelisted
local is_allowed=false
for allowed in "!allowed_domains[@]"; do
if [[ "$hostname" == *"$allowed"* || "$remote" == *"$allowed"* ]]; then
is_allowed=true
break
fi
done
if [[ "$is_allowed" == false ]]; then
trigger_kill "unauthorized outbound network call to hostname (remote) from PID pid"
return 1
fi
done <<< "$connections"
done <<< "$agent_pids"
return 0
}
# ---------------------------------------------------------------------------
# Check 4: Sandbox escape — file writes outside the designated workspace
# ---------------------------------------------------------------------------
check_file_writes() {
# Skip if no workspace is configured
[[ -n "$WORKSPACE" ]] || return 0
local agent_pids
agent_pids=$(pgrep -f "OPENCLAW_PGREP_PATTERN" 2>/dev/null || true)
[[ -n "$agent_pids" ]] || return 0
# Use lsof to check for open file descriptors in write mode outside workspace
while IFS= read -r pid; do
[[ -n "$pid" ]] || continue
# Find files opened for writing by this process
local write_files
write_files=$(lsof -p "$pid" 2>/dev/null | awk '$4 ~ /[0-9]+w/' | awk '{print $9}' || true)
while IFS= read -r filepath; do
[[ -n "$filepath" ]] || continue
# Resolve to absolute path
local abs_path
abs_path=$(realpath "$filepath" 2>/dev/null || echo "$filepath")
# Check if the file is outside the workspace (allow /tmp and /dev)
if [[ "$abs_path" != "WORKSPACE"* && \
"$abs_path" != /tmp* && \
"$abs_path" != /dev* && \
"$abs_path" != /proc* ]]; then
trigger_kill "process PID pid writing outside workspace: abs_path (workspace: WORKSPACE)"
return 1
fi
done <<< "$write_files"
done <<< "$agent_pids"
return 0
}
# ---------------------------------------------------------------------------
# Daemon management
# ---------------------------------------------------------------------------
start_watchdog() {
# Check if already running
if [[ -f "$PID_FILE" ]]; then
local existing_pid
existing_pid=$(cat "$PID_FILE")
if kill -0 "$existing_pid" 2>/dev/null; then
echo "Watchdog is already running (PID existing_pid)."
exit 0
else
# Stale PID file — clean it up
rm -f "$PID_FILE"
fi
fi
echo "Starting DeadClaw watchdog..."
wlog "Watchdog starting. Check interval: CHECK_INTERVALs. Dry run: DRY_RUN"
wlog "Thresholds: runtime=MAX_RUNTIME_MINmin, tokens=MAX_TOKENS/TOKEN_WINDOW_MINmin"
# Fork into the background
(
# Clean up PID file on exit (TERM from kill command, INT from Ctrl-C)
trap 'wlog "Watchdog stopped by signal."; rm -f "$PID_FILE"; exit 0' TERM INT
# Grace period: wait before first check to let sessions settle after restart.
# This prevents false triggers on pre-existing sessions that were restored.
wlog "Grace period: waiting GRACE_PERIODs before first check..."
sleep "$GRACE_PERIOD" &
wait $! 2>/dev/null || true
while true; do
# Run all checks. If any check triggers a kill, the watchdog stops
# (because kill.sh stops the watchdog as part of its cleanup).
wlog "CHECK: scanning..."
check_runtime || { wlog "Watchdog stopping after auto-trigger."; exit 0; }
check_token_spend || { wlog "Watchdog stopping after auto-trigger."; exit 0; }
check_network || { wlog "Watchdog stopping after auto-trigger."; exit 0; }
check_file_writes || { wlog "Watchdog stopping after auto-trigger."; exit 0; }
wlog "CHECK: all clear"
# Sleep in background + wait makes the loop interruptible by signals.
# A plain 'sleep 60' blocks signal delivery until it completes.
sleep "$CHECK_INTERVAL" &
wait $! 2>/dev/null || true
done
) &
local daemon_pid=$!
echo "$daemon_pid" > "$PID_FILE"
echo "Watchdog started (PID daemon_pid). Logging to LOG_FILE."
}
stop_watchdog() {
if [[ -f "$PID_FILE" ]]; then
local pid
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
wlog "Watchdog stopped (PID pid)"
echo "Watchdog stopped (PID pid)."
else
echo "Watchdog was not running (stale PID file)."
fi
rm -f "$PID_FILE"
else
echo "Watchdog is not running (no PID file found)."
fi
}
watchdog_status() {
if [[ -f "$PID_FILE" ]]; then
local pid
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "Watchdog is running (PID pid)."
echo " Check interval: CHECK_INTERVALs"
echo " Max runtime: MAX_RUNTIME_MINmin"
echo " Max tokens: MAX_TOKENS / TOKEN_WINDOW_MINmin"
echo " Dry run: DRY_RUN"
return 0
else
echo "Watchdog is not running (stale PID file)."
rm -f "$PID_FILE"
return 1
fi
else
echo "Watchdog is not running."
return 1
fi
}
# ---------------------------------------------------------------------------
# Main entry point
# ---------------------------------------------------------------------------
case "ACTION" in
start)
start_watchdog
;;
stop)
stop_watchdog
;;
status)
watchdog_status
;;
*)
echo "Usage: watchdog.sh {start|stop|status} [--dry-run]"
exit 1
;;
esac
FILE:ui/deadclaw-button.html
<!DOCTYPE html>
<!--
DeadClaw — WebChat Kill Button Widget
A persistent red button for the OpenClaw WebChat dashboard. When clicked,
it calls kill.sh via OpenClaw's WebChat API hooks and shows confirmation.
Embedding:
OpenClaw.WebChat.registerWidget('deadclaw-button', {
src: 'skills/deadclaw/ui/deadclaw-button.html',
position: 'top-bar',
persistent: true
});
If WebChat hooks are unavailable, the button shows an error with manual
instructions instead of silently failing.
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeadClaw Kill Switch</title>
<style>
/* ----------------------------------------------------------------
DeadClaw button — dark, stark, high contrast.
This is a security tool. The design communicates urgency.
---------------------------------------------------------------- */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
background: #0a0a0a;
color: #e0e0e0;
min-height: 60px;
}
.deadclaw-container {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 12px;
gap: 12px;
background: #0a0a0a;
border-bottom: 1px solid #1a1a1a;
}
/* Pulsing red dot — shows agent activity */
.status-indicator {
position: relative;
width: 12px;
height: 12px;
flex-shrink: 0;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #cc0000;
}
.status-dot.active {
background: #ff0000;
animation: pulse 2s ease-in-out infinite;
}
.status-dot.idle {
background: #444;
animation: none;
}
.status-dot.killed {
background: #ff0000;
animation: none;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
box-shadow: 0 0 4px rgba(255, 0, 0, 0.4);
}
50% {
opacity: 0.5;
box-shadow: 0 0 12px rgba(255, 0, 0, 0.8);
}
}
/* The kill button itself */
.kill-button {
flex: 1;
max-width: 600px;
padding: 12px 24px;
background: #cc0000;
color: #ffffff;
border: 2px solid #ff0000;
border-radius: 4px;
font-family: inherit;
font-size: 14px;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
cursor: pointer;
transition: background 0.15s ease, transform 0.1s ease;
user-select: none;
}
.kill-button:hover {
background: #e60000;
transform: scale(1.01);
}
.kill-button:active {
background: #990000;
transform: scale(0.99);
}
.kill-button:disabled {
background: #333;
border-color: #555;
color: #888;
cursor: not-allowed;
transform: none;
}
/* Confirmation state — after a successful kill */
.kill-button.confirmed {
background: #1a1a1a;
border-color: #ff0000;
color: #ff0000;
cursor: default;
}
/* Error state — when WebChat hooks aren't available */
.kill-button.error {
background: #1a1a1a;
border-color: #ff6600;
color: #ff6600;
}
/* Status text below the button */
.status-text {
font-size: 11px;
color: #666;
text-align: center;
padding-top: 4px;
font-family: monospace;
min-height: 16px;
}
.status-text.alert {
color: #ff0000;
}
.status-text.success {
color: #00cc44;
}
</style>
</head>
<body>
<div class="deadclaw-container">
<div class="status-indicator">
<div class="status-dot active" id="statusDot"></div>
</div>
<button class="kill-button" id="killButton" onclick="handleKill()">
Kill All Agents
</button>
</div>
<div class="status-text" id="statusText">DeadClaw armed</div>
<script>
// -----------------------------------------------------------------------
// DeadClaw WebChat Button Logic
//
// This script tries to use OpenClaw's WebChat API hooks to execute the
// kill script. If those hooks aren't available (e.g., running outside
// WebChat), it degrades gracefully with manual instructions.
// -----------------------------------------------------------------------
var state = 'armed'; // armed | killing | confirmed | error
// Check if OpenClaw WebChat hooks are available
function hasWebChatHooks() {
return (typeof window.OpenClaw !== 'undefined' &&
typeof window.OpenClaw.exec === 'function');
}
// Execute the kill via OpenClaw's WebChat API
function executeKill() {
return new Promise(function(resolve, reject) {
if (!hasWebChatHooks()) {
reject(new Error('WebChat hooks not available'));
return;
}
// Call kill.sh through OpenClaw's exec interface
// REPLACE_THIS: Adjust the path if your skill is installed elsewhere
window.OpenClaw.exec({
command: 'skills/deadclaw/scripts/kill.sh',
args: ['--trigger-source', 'webchat', '--trigger-method', 'button'],
timeout: 30000
}).then(function(result) {
resolve(result);
}).catch(function(err) {
reject(err);
});
});
}
// Main click handler
function handleKill() {
if (state === 'killing' || state === 'confirmed') return;
var button = document.getElementById('killButton');
var statusText = document.getElementById('statusText');
var statusDot = document.getElementById('statusDot');
// Show killing state
state = 'killing';
button.disabled = true;
button.textContent = 'Killing...';
statusText.textContent = 'Sending kill signal...';
statusText.className = 'status-text alert';
executeKill().then(function(result) {
// Success — show confirmation with timestamp and kill count
state = 'confirmed';
button.disabled = true;
button.className = 'kill-button confirmed';
// Parse the result for timestamp and process count
var output = (result && result.output) ? result.output : '';
var killTimestamp = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
var processCount = '?';
// Try to extract process count from kill.sh output (format: "X processes killed")
var countMatch = output.match(/(\d+)\s+processes?\s+killed/i);
if (countMatch) {
processCount = countMatch[1];
}
button.textContent = 'Stopped — ' + processCount + ' killed at ' + killTimestamp;
var summary = output || 'Kill complete. ' + processCount + ' processes killed.';
statusText.textContent = summary;
statusText.className = 'status-text success';
statusDot.className = 'status-dot killed';
// Reset after 30 seconds so the button can be used again
setTimeout(function() {
resetButton();
}, 30000);
}).catch(function(err) {
// WebChat hooks unavailable — show manual instructions
state = 'error';
button.disabled = false;
button.className = 'kill-button error';
button.textContent = 'Manual Kill Required';
statusText.innerHTML =
'WebChat hooks unavailable. Run manually: ' +
'<code style="color:#ff6600">./scripts/kill.sh</code> ' +
'or send "kill" to any connected channel.';
statusText.className = 'status-text alert';
});
}
// Reset button to armed state
function resetButton() {
state = 'armed';
var button = document.getElementById('killButton');
var statusText = document.getElementById('statusText');
var statusDot = document.getElementById('statusDot');
button.disabled = false;
button.className = 'kill-button';
button.textContent = 'Kill All Agents';
statusText.textContent = 'DeadClaw armed';
statusText.className = 'status-text';
statusDot.className = 'status-dot active';
}
// On load, check if hooks are available and update status accordingly
(function init() {
if (!hasWebChatHooks()) {
var statusText = document.getElementById('statusText');
statusText.textContent = 'DeadClaw armed (standalone mode — WebChat hooks not detected)';
}
})();
</script>
</body>
</html>