Skill: camopel/openclaw-claw-guard (version 1.1.0)

--- name: claw-guard description: System-level watchdog for OpenClaw gateway restarts and sub-agent task PIDs. Monitors registered PIDs and optional log/directory freshness. Auto-reverts config on failed gateway restarts. Requires explicit registration — does NOT auto-discover. Use when running long background tasks or before gateway restarts. --- # ClawGuard — Task & Gateway Watchdog A lightweight service that monitors **registered** events: 1. **Sub-agent task PIDs** — if PID dies → notify and remove. If log/dir stale → alert and remove. 2. **Gateway restarts** — if restart fails → revert config backups (newest to oldest) → retry → notify. **ClawGuard only monitors what is explicitly registered.** It does not auto-discover. ## Install ```bash cd <skill-dir> bash scripts/install.sh ``` Installs: - **Daemon**: systemd user service (Linux) or launchd agent (macOS) — `Restart=always`, auto-starts on boot - **CLI**: `claw-guard` in `~/.local/bin/` - **Data**: `~/.openclaw/workspace/tools/claw-guard/` ## OpenClaw Integration (Recommended) ### 1. Auto-register gateway restarts Add `ExecStartPre` to your gateway service so every restart (manual, crash, or `Restart=always`) is automatically registered: ```ini # ~/.config/systemd/user/openclaw-gateway.service [Service] ExecStartPre=/home/<user>/.local/bin/claw-guard register-restart ExecStart=... ``` Then reload: `systemctl --user daemon-reload` Now every gateway restart automatically: - Snapshots the current config (rotates up to 5 backups) - Watches for the gateway to come back - If it fails → reverts config backups newest-to-oldest → notifies default channel **No manual `claw-guard register-restart` needed — systemd handles it.** ### 2. Add task execution rules to AGENTS.md Add these rules so the agent always registers its work: ```markdown ## Task Execution Rules (MANDATORY) ### Sub-agent requirement - **Any exec/tool call that might take >5s → sub-agent** (`sessions_spawn`). Main agent stays responsive. - **Complex or unpredictable tasks → always sub-agent.** Even if they might be fast. If you can't guarantee it won't block, delegate it. - **Only run in main agent** if certain it won't block I/O (quick file reads, short `grep`, `git status`, `claw-guard status`, etc.) ### ClawGuard registration (MANDATORY for all sub-agents) Every sub-agent and background process **must** be registered: ```bash claw-guard register --id "<descriptive-id>" --pid <PID> \ --target "<channel where task was requested>" \ --log "/path/to/logfile" --timeout 180 \ --command "<what it does>" ``` - `--target`: same channel/room where the user asked for the task - `--log` and `--timeout`: optional but recommended for long tasks - If PID dies → claw-guard notifies the target channel and removes the entry - If log goes stale → claw-guard notifies and removes ### Gateway restarts - **Never restart the gateway while tasks are running** — it kills all sub-agents - Gateway service has `ExecStartPre=claw-guard register-restart` — automatic - No manual registration needed for restarts ``` ### 3. How it works end-to-end **Sub-agent task flow:** 1. User requests a long-running task 2. Agent spawns sub-agent → gets PID 3. Agent runs: `claw-guard register --id "task-name" --pid $PID --target "room:..." --command "..."` 4. If PID dies → claw-guard notifies the target channel → agent confirms result with user 5. If log goes stale → claw-guard alerts → agent investigates **Gateway restart flow:** 1. Gateway restarts (manual, crash, or auto) 2. `ExecStartPre` runs `claw-guard register-restart` → config backed up 3. Gateway starts successfully → claw-guard logs `✅ Gateway restart succeeded` → watch cleared 4. Gateway fails to start → claw-guard tries config backups → notifies default channel ## CLI Reference ### Register a task ```bash claw-guard register --id "benchmark-q8" --pid 12345 \ --target "room:!abc:server" \ --log "/path/to/task.log" --timeout 180 \ --command "python3 benchmark.py" # Or watch a directory for new file creation: claw-guard register --id "export-gguf" --pid 12345 \ --target "room:!abc:server" \ --watch-dir "/path/to/output/" --timeout 300 \ --command "export_gguf.py" ``` | Flag | Required | Description | |------|----------|-------------| | `--id` | yes | Unique task identifier | | `--pid` | yes | Process ID to watch | | `--target` | yes | Notification target (see formats below) | | `--log` | no | Log file path — checks mtime only | | `--watch-dir` | no | Directory — checks newest file mtime | | `--timeout` | no | Stale threshold in seconds (default: 180) | | `--command` | no | Description included in notifications | ### Register a gateway restart ```bash claw-guard register-restart [--target "room:!abc:server"] ``` No `--target` needed — sends to OpenClaw's default channel. Pass `--target` to override. ### Manage ```bash claw-guard status # Show tasks, restart watch, config backups claw-guard remove --id X # Remove a task claw-guard clear-done # Remove completed/gone tasks ``` ## Behavior ### Check cycle (every 15s) 1. **Gateway restart**: if registered and gateway not active after 30s → revert + retry + notify 2. **PID check**: if PID gone → notify target → remove entry 3. **Log/dir freshness**: if mtime exceeds timeout → notify target → remove entry ### Deduplication After notifying, the registered entry is **removed from the registry**. Once removed, it can't fire again. No dedup tracking needed. ### Restart / reboot behavior On service restart or system reboot: - **All registered tasks are cleared** — nothing carries over - **Config backups persist** on disk (only thing that survives) This is by design: after a reboot, all monitored processes are gone anyway. The agent must re-register any new tasks. ## Notification Targets Any format `openclaw message send --target` accepts: - `room:!roomId:server` (Matrix) - `telegram:chatid` - `discord:#channel` - `slack:#channel` Gateway restart alerts with no `--target` are sent without a target flag, letting OpenClaw route to the default channel. ## Design Principles - **Registration-based, not auto-discovery** — only watches what's explicitly registered - **Notify once, then remove** — no duplicate alerts, no stale state - **In-memory state** — registry clears on service restart (clean slate) - **Disk persistence only for config backups** — the only thing worth keeping across restarts - **Cross-platform** — Linux (systemd) and macOS (launchd) - **Minimal overhead** — ~7MB RAM, negligible CPU

path: scripts/claw-guard-cli.py
content: #!/usr/bin/env python3
"""
ClawGuard CLI — Register tasks for PID + log monitoring.
"""
import json, os, sys, argparse, time, shutil, glob
from pathlib import Path

DATA_DIR = Path(os.path.expanduser("~/.openclaw/workspace/tools/claw-guard"))
STATE_FILE = DATA_DIR / "state.json"
OC_CONFIG = Path(os.path.expanduser("~/.openclaw/openclaw.json"))
BACKUP_DIR = DATA_DIR / "config_backups"
MAX_BACKUPS = 5


def load():
    if STATE_FILE.exists():
        try:
            return json.loads(STATE_FILE.read_text())
        except:
            pass
    return {"tasks": {}}


def save(state):
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    tmp = STATE_FILE.with_suffix(".tmp")
    tmp.write_text(json.dumps(state, indent=2))
    tmp.rename(STATE_FILE)


def cmd_register(args):
    state = load()
    tasks = state.get("tasks", {})
    tasks[args.id] = {
        "pid": args.pid,
        "target": args.target,
        "log": args.log or "",
        "watch_dir": args.watch_dir or "",
        "timeout": args.timeout,
        "command": args.command or "",
        "registered_at": time.time(),
        "status": "watching",
        "stale_notified": False,
        "last_dir_count": None,
    }
    state["tasks"] = tasks
    save(state)
    print(f"🛡️ Registered '{args.id}' (PID {args.pid}) → {args.target}")
    if args.log:
        print(f"   Log: {args.log} (stale after {args.timeout}s)")
    if args.watch_dir:
        print(f"   Watch dir: {args.watch_dir} (stale after {args.timeout}s)")


def cmd_register_restart(args):
    """Snapshot current config and tell ClawGuard to watch the gateway restart."""
    state = load()
    BACKUP_DIR.mkdir(parents=True, exist_ok=True)

    # Save timestamped backup
    if OC_CONFIG.exists():
        ts = time.strftime("%Y%m%d_%H%M%S")
        backup = BACKUP_DIR / f"openclaw_{ts}.json"
        shutil.copy2(str(OC_CONFIG), str(backup))
        print(f"📸 Config backed up → {backup.name}")

        # Prune old backups, keep newest MAX_BACKUPS
        backups = sorted(BACKUP_DIR.glob("openclaw_*.json"), key=lambda f: f.stat().st_mtime, reverse=True)
        for old in backups[MAX_BACKUPS:]:
            old.unlink()
            print(f"   Pruned old backup: {old.name}")
    else:
        print("⚠️ No openclaw.json found to back up")

    state["watching_restart"] = True
    state["restart_registered_at"] = time.time()
    state["restart_target"] = args.target or ""
    save(state)
    print("🛡️ Gateway restart registered — ClawGuard will watch for failure")


def cmd_status(args):
    state = load()
    restart = state.get("watching_restart", False)
    print(f"Gateway restart watch: {'ACTIVE' if restart else 'off'}")
    # Show available backups
    if BACKUP_DIR.exists():
        backups = sorted(BACKUP_DIR.glob("openclaw_*.json"), key=lambda f: f.stat().st_mtime, reverse=True)
        if backups:
            print(f"Config backups ({len(backups)}):")
            for b in backups:
                age_min = (time.time() - b.stat().st_mtime) / 60
                print(f"  {b.name} ({age_min:.0f}m ago)")
    tasks = state.get("tasks", {})
    if not tasks:
        print("No tasks registered.")
        return
    for tid, t in tasks.items():
        status = t.get("status", "?")
        icon = {"watching": "👀", "pid_gone": "💀"}.get(status, "❓")
        pid = t.get("pid", "?")
        age = ""
        if t.get("registered_at"):
            mins = (time.time() - t["registered_at"]) / 60
            age = f" ({mins:.0f}m)"
        target = t.get("target", "?")
        print(f"  {icon} {tid:25s} PID={pid:>8} {status:10s}{age}  → {target[:30]}  {t.get('command', '')[:30]}")


def cmd_remove(args):
    state = load()
    tasks = state.get("tasks", {})
    if args.id in tasks:
        del tasks[args.id]
        state["tasks"] = tasks
        save(state)
        print(f"Removed '{args.id}'")
    else:
        print(f"Not found: '{args.id}'")


def cmd_clear_done(args):
    state = load()
    tasks = state.get("tasks", {})
    active = {k: v for k, v in tasks.items() if v.get("status") == "watching"}
    removed = len(tasks) - len(active)
    state["tasks"] = active
    save(state)
    print(f"Cleared {removed}, {len(active)} active")


def main():
    parser = argparse.ArgumentParser(prog="claw-guard")
    sub = parser.add_subparsers(dest="cmd")

    p = sub.add_parser("register", help="Register a task for monitoring")
    p.add_argument("--id", required=True)
    p.add_argument("--pid", type=int, required=True)
    p.add_argument("--target", required=True, help="Notification target (room:!id:server, telegram:id, etc.)")
    p.add_argument("--log", default="", help="Log file path (checks mtime only)")
    p.add_argument("--watch-dir", default="", dest="watch_dir", help="Directory to watch for new file creation")
    p.add_argument("--timeout", type=int, default=180, help="Stale timeout in seconds for log/dir (default: 180)")
    p.add_argument("--command", default="", help="Command description")

    p_restart = sub.add_parser("register-restart", help="Snapshot config and watch gateway restart")
    p_restart.add_argument("--target", default="", help="Notification target for restart failure alerts")
    sub.add_parser("status", help="Show watched tasks")

    p_rm = sub.add_parser("remove", help="Remove a task")
    p_rm.add_argument("--id", required=True)

    sub.add_parser("clear-done", help="Remove non-watching tasks")

    args = parser.parse_args()
    cmds = {"register": cmd_register, "register-restart": cmd_register_restart,
            "status": cmd_status, "remove": cmd_remove, "clear-done": cmd_clear_done}
    if args.cmd in cmds:
        cmds[args.cmd](args)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()


path: scripts/claw-guard.py
content: #!/usr/bin/env python3
"""
ClawGuard daemon — monitors registered PIDs, log/dir freshness, and gateway restarts.
Cross-platform: Linux (systemd) and macOS (launchd).

After notifying, the registered entry is removed — no duplicate notifications possible.
On service restart, registry is cleared (all monitored processes are gone anyway).
Only config backups persist on disk.
"""
import json, os, time, subprocess, sys, signal, shutil, platform
from pathlib import Path
from datetime import datetime

DATA_DIR = Path(os.path.expanduser("~/.openclaw/workspace/tools/claw-guard"))
REGISTRY = DATA_DIR / "state.json"
LOG_FILE = DATA_DIR / "claw-guard.log"
OC_CONFIG = Path(os.path.expanduser("~/.openclaw/openclaw.json"))
BACKUP_DIR = DATA_DIR / "config_backups"
CHECK_INTERVAL = 15
GATEWAY_RECOVER_TIMEOUT = 30
IS_MACOS = platform.system() == "Darwin"

DATA_DIR.mkdir(parents=True, exist_ok=True)


def log(msg):
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] {msg}"
    print(line, flush=True)
    with open(LOG_FILE, "a") as f:
        f.write(line + "\n")
    if LOG_FILE.exists() and LOG_FILE.stat().st_size > 500_000:
        LOG_FILE.rename(LOG_FILE.with_suffix(".log.old"))


def read_registry():
    if not REGISTRY.exists():
        return {"tasks": {}, "watching_restart": False}
    try:
        return json.loads(REGISTRY.read_text())
    except:
        return {"tasks": {}, "watching_restart": False}


def write_registry(data):
    tmp = REGISTRY.with_suffix(".tmp")
    tmp.write_text(json.dumps(data, indent=2))
    tmp.rename(REGISTRY)


def notify(target, message):
    try:
        if target:
            cmd = ["openclaw", "message", "send", "--target", target, "--message", message]
            if ":" in target and not target.startswith("room:"):
                parts = target.split(":", 1)
                cmd = ["openclaw", "message", "send", "--channel", parts[0],
                       "--target", parts[1], "--message", message]
        else:
            # No target — let OpenClaw route to default channel
            cmd = ["openclaw", "message", "send", "--message", message]
        subprocess.run(cmd, capture_output=True, text=True, timeout=15)
    except Exception as e:
        log(f"  Notify error: {e}")


def pid_alive(pid):
    try:
        os.kill(int(pid), 0)
        return True
    except (OSError, ProcessLookupError, ValueError):
        return False


def gateway_active():
    try:
        if IS_MACOS:
            r = subprocess.run(["launchctl", "list"], capture_output=True, text=True, timeout=5)
            return "openclaw" in r.stdout.lower()
        else:
            r = subprocess.run(
                ["systemctl", "--user", "is-active", "openclaw-gateway.service"],
                capture_output=True, text=True, timeout=5)
            return r.stdout.strip() == "active"
    except:
        return False


def gateway_status_error():
    try:
        if IS_MACOS:
            r = subprocess.run(["launchctl", "list", "com.openclaw.gateway"],
                              capture_output=True, text=True, timeout=10)
            return (r.stdout or r.stderr)[-500:]
        else:
            r = subprocess.run(
                ["systemctl", "--user", "status", "openclaw-gateway.service"],
                capture_output=True, text=True, timeout=10)
            return r.stdout[-500:] if r.stdout else "no output"
    except:
        return "could not read status"


def gateway_journal():
    try:
        if IS_MACOS:
            log_dir = Path("/tmp/openclaw")
            if log_dir.exists():
                logs = sorted(log_dir.glob("openclaw-*.log"), key=lambda f: f.stat().st_mtime, reverse=True)
                if logs:
                    return logs[0].read_text()[-500:]
            return ""
        else:
            r = subprocess.run(
                ["journalctl", "--user-unit", "openclaw-gateway.service", "-n", "20", "--no-pager"],
                capture_output=True, text=True, timeout=10)
            return r.stdout[-500:] if r.stdout else ""
    except:
        return ""


def restart_gateway():
    try:
        if IS_MACOS:
            uid = os.getuid()
            subprocess.run(["launchctl", "kickstart", "-k", f"gui/{uid}/com.openclaw.gateway"],
                          capture_output=True, text=True, timeout=30)
        else:
            subprocess.run(["systemctl", "--user", "restart", "openclaw-gateway.service"],
                          capture_output=True, text=True, timeout=30)
    except Exception as e:
        log(f"  Restart command failed: {e}")


def get_config_backups():
    if not BACKUP_DIR.exists():
        return []
    return sorted(BACKUP_DIR.glob("openclaw_*.json"), key=lambda f: f.stat().st_mtime, reverse=True)


def try_revert_and_restart():
    backups = get_config_backups()
    if not backups:
        log("  No config backups available")
        return False
    for i, backup in enumerate(backups):
        log(f"  Trying backup {i+1}/{len(backups)}: {backup.name}")
        try:
            shutil.copy2(str(backup), str(OC_CONFIG))
        except Exception as e:
            log(f"  Copy failed: {e}")
            continue
        restart_gateway()
        time.sleep(5)
        if gateway_active():
            log(f"  ✅ Gateway recovered with {backup.name}")
            return True
        log(f"  ❌ Still down with {backup.name}")
    return False


def check_restart(reg):
    if not reg.get("watching_restart"):
        return

    if gateway_active():
        log("✅ Gateway restart succeeded")
        # Remove the restart watch
        reg["watching_restart"] = False
        reg.pop("restart_registered_at", None)
        reg.pop("restart_target", None)
        write_registry(reg)
        return

    registered = reg.get("restart_registered_at", 0)
    if time.time() - registered < GATEWAY_RECOVER_TIMEOUT:
        return

    error = gateway_status_error()
    journal = gateway_journal()
    log("🚨 Gateway restart FAILED")

    recovered = try_revert_and_restart()
    target = reg.get("restart_target", "")

    detail = error[:300]
    if journal:
        detail += f"\n\nJournal:\n{journal[:200]}"

    if recovered:
        notify(target, f"🚨 Gateway restart failed — ClawGuard reverted config and recovered.\n\nWhy:\n{detail}")
    else:
        notify(target, f"🚨 Gateway DOWN — all config backups failed! Manual intervention needed.\n\nWhy:\n{detail}")

    # Remove the restart watch after notifying — prevents duplicate
    reg["watching_restart"] = False
    reg.pop("restart_registered_at", None)
    reg.pop("restart_target", None)
    write_registry(reg)


def check_tasks(reg):
    tasks = reg.get("tasks", {})
    to_remove = []

    for task_id, task in tasks.items():
        if task.get("status") != "watching":
            continue

        pid = task.get("pid")
        target = task.get("target", "")
        timeout = task.get("timeout", 180)

        # PID gone → notify and remove
        if pid and not pid_alive(pid):
            log(f"📢 Task '{task_id}' — PID {pid} gone")
            notify(target,
                   f"📢 Task '{task_id}' — PID {pid} disappeared.\n"
                   f"Command: {task.get('command', '?')}\n"
                   f"Please confirm: success or failure?")
            to_remove.append(task_id)
            continue

        # Log freshness → notify and remove
        log_path = task.get("log")
        if log_path and timeout > 0:
            lp = Path(os.path.expanduser(log_path))
            if lp.exists() and (time.time() - lp.stat().st_mtime) > timeout:
                age = int(time.time() - lp.stat().st_mtime)
                log(f"📢 Task '{task_id}' — log stale {age}s")
                notify(target,
                       f"📢 Task '{task_id}' — log not updated for {age}s (timeout: {timeout}s).\n"
                       f"PID {pid}: {'alive' if pid_alive(pid) else 'gone'}")
                to_remove.append(task_id)
                continue

        # Dir freshness → notify and remove
        watch_dir = task.get("watch_dir")
        if watch_dir and timeout > 0:
            dp = Path(os.path.expanduser(watch_dir))
            if dp.exists() and dp.is_dir():
                newest = max((f.stat().st_mtime for f in dp.iterdir() if f.is_file()), default=0)
                if newest > 0 and (time.time() - newest) > timeout:
                    age = int(time.time() - newest)
                    log(f"📢 Task '{task_id}' — dir stale {age}s")
                    notify(target,
                           f"📢 Task '{task_id}' — no new files in {watch_dir} for {age}s (timeout: {timeout}s).\n"
                           f"PID {pid}: {'alive' if pid_alive(pid) else 'gone'}")
                    to_remove.append(task_id)
                    continue

    # Remove notified tasks — prevents duplicates
    if to_remove:
        for tid in to_remove:
            del tasks[tid]
        reg["tasks"] = tasks
        write_registry(reg)


def handle_signal(signum, frame):
    log(f"ClawGuard signal {signum}, exiting")
    sys.exit(0)


def main():
    signal.signal(signal.SIGTERM, handle_signal)
    signal.signal(signal.SIGINT, handle_signal)
    log(f"🛡️ ClawGuard started ({platform.system()})")

    # Clear registry on start — all old processes are gone after reboot
    write_registry({"tasks": {}, "watching_restart": False})
    log("  Registry cleared (fresh start)")

    while True:
        try:
            reg = read_registry()
            check_restart(reg)
            if reg.get("tasks"):
                check_tasks(reg)
        except Exception as e:
            log(f"Error: {e}")
        time.sleep(CHECK_INTERVAL)


if __name__ == "__main__":
    main()


path: scripts/install.sh
content: #!/bin/bash
set -e
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
GUARD_DIR="$HOME/.openclaw/workspace/tools/claw-guard"

echo "🛡️ Installing ClawGuard..."
mkdir -p "$GUARD_DIR"
cp "$SKILL_DIR/scripts/claw-guard.py" "$GUARD_DIR/claw-guard.py"
cp "$SKILL_DIR/scripts/claw-guard-cli.py" "$GUARD_DIR/claw-guard-cli.py"
chmod +x "$GUARD_DIR/claw-guard.py" "$GUARD_DIR/claw-guard-cli.py"

PYTHON="$(command -v python3 2>/dev/null || true)"
[ -z "$PYTHON" ] && echo "❌ Python3 not found in PATH" && exit 1
echo "  Python: $PYTHON"

# CLI wrapper
mkdir -p "$HOME/.local/bin"
cat > "$HOME/.local/bin/claw-guard" << CLI
#!/bin/bash
exec $PYTHON $GUARD_DIR/claw-guard-cli.py "\$@"
CLI
chmod +x "$HOME/.local/bin/claw-guard"

# Detect OS and install service
OS="$(uname -s)"
CURRENT_PATH="$PATH"

if [ "$OS" = "Darwin" ]; then
    # macOS — launchd plist
    PLIST_DIR="$HOME/Library/LaunchAgents"
    PLIST="$PLIST_DIR/com.openclaw.claw-guard.plist"
    mkdir -p "$PLIST_DIR"
    cat > "$PLIST" << PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.openclaw.claw-guard</string>
    <key>ProgramArguments</key>
    <array>
        <string>$PYTHON</string>
        <string>$GUARD_DIR/claw-guard.py</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>$GUARD_DIR/claw-guard.stdout.log</string>
    <key>StandardErrorPath</key>
    <string>$GUARD_DIR/claw-guard.stderr.log</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>HOME</key>
        <string>$HOME</string>
        <key>PATH</key>
        <string>$CURRENT_PATH</string>
    </dict>
</dict>
</plist>
PLIST

    launchctl unload "$PLIST" 2>/dev/null || true
    launchctl load "$PLIST"
    sleep 2
    if launchctl list | grep -q com.openclaw.claw-guard; then
        echo "✅ ClawGuard running (launchd)"
    else
        echo "⚠️ Failed to start — check: launchctl list | grep claw-guard"
    fi

else
    # Linux — systemd user service
    SERVICE_DIR="$HOME/.config/systemd/user"
    mkdir -p "$SERVICE_DIR"
    cat > "$SERVICE_DIR/claw-guard.service" << UNIT
[Unit]
Description=ClawGuard — OpenClaw task watchdog
After=network.target

[Service]
Type=simple
ExecStart=$PYTHON $GUARD_DIR/claw-guard.py
Restart=always
RestartSec=10
Environment=HOME=$HOME
Environment=PATH=$CURRENT_PATH

[Install]
WantedBy=default.target
UNIT

    systemctl --user daemon-reload
    systemctl --user enable claw-guard.service
    systemctl --user restart claw-guard.service
    sleep 2
    if systemctl --user is-active claw-guard.service > /dev/null 2>&1; then
        echo "✅ ClawGuard running (systemd)"
    else
        echo "⚠️ Failed to start"
        systemctl --user status claw-guard.service
    fi
fi

echo "  CLI: claw-guard register|register-restart|status|remove|clear-done"
echo "  Log: $GUARD_DIR/claw-guard.log"


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)