Skill: crayfish-ai/feishu-relay (version 3.2.0)

--- name: feishu-relay description: Send Feishu (Lark) notifications via OpenClaw. Core only: send messages, queue, retry. No system modifications by default. --- # Feishu Notifier - Core **Version: 3.0 (Safe Mode)** A minimal, safe Feishu notification bridge for OpenClaw. ## What This Does ✅ **Core (always available)**: - Send messages to Feishu via Open API - JSON structured output - Config via environment or skill config - Retry on network failure ❌ **Not included by default**: - No crontab installation - No systemd service setup - No global `/usr/local/bin/notify` link - No auto-discovery of other skills - No environment injection ## Quick Start ```bash # Configure (environment variables) export FEISHU_APP_ID="cli_xxx" export FEISHU_APP_SECRET="xxx" export FEISHU_RECEIVE_ID="ou_xxx" # Send notification ./run.sh -t "Title" -m "Message body" ``` ## Configuration | Config | Env Variable | Required | Default | |--------|-------------|----------|---------| | appId | FEISHU_APP_ID | Yes | - | | appSecret | FEISHU_APP_SECRET | Yes | - | | receiveId | FEISHU_RECEIVE_ID | Yes | - | | receiveIdType | FEISHU_RECEIVE_ID_TYPE | No | open_id | ## Usage ```bash # Basic ./run.sh -t "Title" -m "Message" # JSON output ./run.sh -t "Title" -m "Message" --json # Test ./run.sh --test ``` ## Output Format ```json { "success": true, "message_id": "om_xxx", "create_time": "1234567890" } ``` ## Safety - No secrets in logs - No system modifications - No root required for basic use - Config file: `chmod 600 config.json`

path: README.md
content: # Feishu Relay

**Safe Mode: Minimal by default, production capabilities opt-in**

A Feishu (Lark) notification bridge for OpenClaw.

---

## Two Modes

### Default Mode (ClawHub Install)

Safe, minimal, no system modifications.

```bash
openclaw skill install feishu-relay
```

**What you get:**
- ✅ Send messages via `./run.sh`
- ✅ JSON structured output
- ✅ Retry on failure
- ✅ No system modifications
- ✅ No root required

**Best for:** Personal computers, shared hosting, testing, ClawHub distribution.

---

### Production Mode (Self-Hosted Server)

For your own Linux server with 24/7 uptime requirements.

```bash
# After base install, enable production capabilities:
./scripts/install-systemd.sh --install --user      # 24/7 daemon
./scripts/install-watchdog.sh --install           # Health monitoring
sudo ./scripts/install-global-notify.sh --install  # /usr/local/bin/notify
```

**What you get (in addition to defaults):**
- 🔧 Systemd service with auto-restart
- 🔧 Watchdog health monitoring
- 🔧 Global `/usr/local/bin/notify` command

**Best for:** Your own server, 24/7 operations, production workloads.

See [docs/production.md](docs/production.md) for full production deployment guide.

---

## Quick Start

### 1. Install

```bash
openclaw skill install feishu-relay
```

### 2. Configure

```bash
export FEISHU_APP_ID="cli_xxxxxxxx"
export FEISHU_APP_SECRET="xxxxxxxx"
export FEISHU_RECEIVE_ID="ou_xxxxxxxx"
```

Or create `config.json`:

```json
{
  "appId": "cli_xxxxxxxx",
  "appSecret": "xxxxxxxx",
  "receiveId": "ou_xxxxxxxx"
}
```

### 3. Test

```bash
./run.sh -t "Test" -m "Hello from feishu-relay"
```

---

## Usage

```bash
# Basic message
./run.sh -t "Title" -m "Message"

# JSON output (for scripting)
./run.sh -t "Title" -m "Message" --json

# With receive ID override
./run.sh -t "Title" -m "Message" --receive-id ou_xxxx --receive-id-type open_id
```

---

## Configuration

| Option | Env Variable | Required | Default |
|--------|-------------|----------|---------|
| App ID | `FEISHU_APP_ID` | Yes | - |
| App Secret | `FEISHU_APP_SECRET` | Yes | - |
| Receive ID | `FEISHU_RECEIVE_ID` | Yes | - |
| ID Type | `FEISHU_RECEIVE_ID_TYPE` | No | `open_id` |

ID types: `open_id`, `user_id`, `chat_id`, `email`

---

## Project Structure

```
feishu-relay/
├── run.sh                 # Main entry point
├── notify                 # CLI wrapper
├── lib/send.py            # Core sending logic
├── config.json.example    # Config example
├── scripts/
│   ├── install-systemd.sh       # Production: systemd service
│   ├── install-watchdog.sh      # Production: health monitoring
│   ├── install-global-notify.sh # Production: global command
│   ├── install-crontab.sh       # Production: cron jobs
│   └── install-discovery.sh     # Scan for other skills
├── docs/
│   ├── advanced.md        # Opt-in capabilities
│   ├── production.md       # Production deployment guide
│   ├── security.md         # Security considerations
│   └── uninstall.md        # Removal guide
└── SKILL.md               # OpenClaw skill metadata
```

---

## Safety

- **No system modifications by default**
- **No root required for basic use**
- **No auto-migration**
- **Production capabilities are opt-in only**

---

## Documentation

| Doc | What |
|-----|------|
| [SKILL.md](SKILL.md) | Quick reference |
| [README.md](README.md) | This file |
| [docs/advanced.md](docs/advanced.md) | Opt-in capabilities |
| [docs/production.md](docs/production.md) | Production deployment |
| [docs/security.md](docs/security.md) | Security guide |
| [docs/uninstall.md](docs/uninstall.md) | Complete removal |

---

## License

MIT-0


path: config.example.json
content: receiveIdType: open_id

path: lib/send.py
content: #!/usr/bin/env python3
"""
Feishu Notifier Core - Production Ready
飞书通知核心模块

Features:
- Secure config handling (env > skill config > default)
- Robust JSON processing with proper escaping
- Automatic retry with exponential backoff
- Structured output (JSON)
- No secrets in logs
"""

import json
import os
import sys
import time
import urllib.request
import urllib.error
import ssl
from typing import Optional, Dict, Any, Tuple

# API Endpoints
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
TOKEN_ENDPOINT = f"{FEISHU_API_BASE}/auth/v3/tenant_access_token/internal"
MESSAGE_ENDPOINT = f"{FEISHU_API_BASE}/im/v1/messages"


class FeishuNotifierError(Exception):
    """Base exception for Feishu Notifier"""
    pass


class ConfigError(FeishuNotifierError):
    """Configuration error"""
    pass


class APIError(FeishuNotifierError):
    """API call error"""
    def __init__(self, message: str, code: Optional[int] = None, response: Optional[Dict] = None):
        super().__init__(message)
        self.code = code
        self.response = response


class FeishuConfig:
    """Configuration manager with secure handling"""
    
    def __init__(self):
        self.app_id: Optional[str] = None
        self.app_secret: Optional[str] = None
        self.receive_id: Optional[str] = None
        self.receive_id_type: str = "open_id"
        self.timeout: int = 30
        self.max_retries: int = 3
        self.retry_delay: int = 2
        
    def load(self) -> "FeishuConfig":
        """Load configuration from multiple sources (env > skill config)"""
        # Priority 1: Environment variables
        self.app_id = os.environ.get("FEISHU_APP_ID") or os.environ.get("FEISHU_appId")
        self.app_secret = os.environ.get("FEISHU_APP_SECRET") or os.environ.get("FEISHU_appSecret")
        self.receive_id = os.environ.get("FEISHU_RECEIVE_ID") or os.environ.get("FEISHU_receiveId")
        self.receive_id_type = os.environ.get("FEISHU_RECEIVE_ID_TYPE", "open_id")
        
        if env_timeout := os.environ.get("FEISHU_TIMEOUT"):
            self.timeout = int(env_timeout)
        if env_retries := os.environ.get("FEISHU_MAX_RETRIES"):
            self.max_retries = int(env_retries)
        if env_delay := os.environ.get("FEISHU_RETRY_DELAY"):
            self.retry_delay = int(env_delay)
        
        # Priority 2: OpenClaw skill config (if available)
        self._load_skill_config()
        
        return self
    
    def _load_skill_config(self):
        """Load from OpenClaw skill config file if exists"""
        # Determine skill directory
        skill_dir = os.environ.get("FEISHU_SKILL_DIR", "")
        
        config_paths = [
            # Environment variable (set by run.sh)
            os.path.join(skill_dir, "config.json") if skill_dir else None,
            # OpenClaw standard locations
            os.path.expanduser("~/.openclaw/skills/feishu-relay/config.json"),
            "/etc/openclaw/skills/feishu-relay/config.json",
            # Fallback: relative to current working directory
            "./config.json",
            # Legacy: .env file in skill directory
            os.path.join(skill_dir, ".env") if skill_dir else None,
        ]
        
        # Filter out None values
        config_paths = [p for p in config_paths if p]
        
        for config_path in config_paths:
            if os.path.exists(config_path):
                try:
                    with open(config_path, 'r') as f:
                        config = json.load(f)
                    
                    # Only use skill config if env not set
                    if not self.app_id:
                        self.app_id = config.get("appId")
                    if not self.app_secret:
                        self.app_secret = config.get("appSecret")
                    if not self.receive_id:
                        self.receive_id = config.get("receiveId")
                    if "receiveIdType" in config and self.receive_id_type == "open_id":
                        self.receive_id_type = config.get("receiveIdType", "open_id")
                    if "timeout" in config:
                        self.timeout = config.get("timeout", 30)
                    if "maxRetries" in config:
                        self.max_retries = config.get("maxRetries", 3)
                    if "retryDelay" in config:
                        self.retry_delay = config.get("retryDelay", 2)
                    break
                except (json.JSONDecodeError, IOError):
                    continue
    
    def validate(self) -> None:
        """Validate configuration, raise ConfigError if invalid"""
        errors = []
        
        if not self.app_id:
            errors.append("appId is required (set FEISHU_APP_ID env or skill config)")
        elif not self.app_id.startswith("cli_"):
            errors.append(f"appId should start with 'cli_' (got: {self.app_id[:10]}...)")
            
        if not self.app_secret:
            errors.append("appSecret is required (set FEISHU_APP_SECRET env or skill config)")
        elif len(self.app_secret) < 20:
            errors.append("appSecret seems too short (should be ~32+ chars)")
            
        if not self.receive_id:
            errors.append("receiveId is required (set FEISHU_RECEIVE_ID env or skill config)")
            
        valid_id_types = ["open_id", "user_id", "chat_id", "email"]
        if self.receive_id_type not in valid_id_types:
            errors.append(f"receiveIdType must be one of {valid_id_types}, got: {self.receive_id_type}")
        
        if errors:
            raise ConfigError("; ".join(errors))
    
    def to_safe_dict(self) -> Dict[str, Any]:
        """Return config dict with secrets masked (for logging)"""
        return {
            "app_id": self.app_id[:10] + "..." if self.app_id else None,
            "app_secret": "***REDACTED***" if self.app_secret else None,
            "receive_id": self.receive_id[:10] + "..." if self.receive_id else None,
            "receive_id_type": self.receive_id_type,
            "timeout": self.timeout,
            "max_retries": self.max_retries,
            "retry_delay": self.retry_delay
        }


class FeishuNotifier:
    """Main notifier class with retry logic"""
    
    def __init__(self, config: FeishuConfig):
        self.config = config
        self._token: Optional[str] = None
        self._token_expires: float = 0
        
        # Create SSL context that verifies certificates
        self._ssl_context = ssl.create_default_context()
    
    def _make_request(
        self, 
        url: str, 
        method: str = "GET", 
        headers: Optional[Dict[str, str]] = None,
        data: Optional[bytes] = None,
        retry_count: int = 0
    ) -> Tuple[int, Dict[str, Any]]:
        """Make HTTP request with retry logic"""
        
        req_headers = headers or {}
        req = urllib.request.Request(
            url, 
            data=data, 
            headers=req_headers, 
            method=method
        )
        
        try:
            with urllib.request.urlopen(
                req, 
                timeout=self.config.timeout,
                context=self._ssl_context
            ) as response:
                body = response.read().decode('utf-8')
                return response.status, json.loads(body) if body else {}
                
        except urllib.error.HTTPError as e:
            body = e.read().decode('utf-8') if e.read() else "{}"
            try:
                error_data = json.loads(body)
            except json.JSONDecodeError:
                error_data = {"error": body}
            return e.code, error_data
            
        except (urllib.error.URLError, TimeoutError) as e:
            if retry_count < self.config.max_retries:
                time.sleep(self.config.retry_delay * (retry_count + 1))
                return self._make_request(url, method, headers, data, retry_count + 1)
            raise APIError(f"Request failed after {self.config.max_retries} retries: {e}")
    
    def get_access_token(self) -> str:
        """Get tenant access token with caching"""
        # Return cached token if still valid (with 60s buffer)
        if self._token and time.time() < self._token_expires - 60:
            return self._token
        
        payload = json.dumps({
            "app_id": self.config.app_id,
            "app_secret": self.config.app_secret
        }).encode('utf-8')
        
        headers = {"Content-Type": "application/json"}
        
        status, response = self._make_request(
            TOKEN_ENDPOINT,
            method="POST",
            headers=headers,
            data=payload
        )
        
        if status != 200 or response.get("code", -1) != 0:
            code = response.get("code", "unknown")
            msg = response.get("msg", "Unknown error")
            raise APIError(f"Failed to get access token: {msg} (code: {code})", code, response)
        
        self._token = response.get("tenant_access_token")
        expire = response.get("expire", 7200)
        self._token_expires = time.time() + expire
        
        return self._token
    
    def send_message(
        self, 
        content: str, 
        title: Optional[str] = None,
        receive_id: Optional[str] = None,
        receive_id_type: Optional[str] = None
    ) -> Dict[str, Any]:
        """Send text message to Feishu"""
        
        [REDACTED_SECRET])
        
        target_id = receive_id or self.config.receive_id
        target_type = receive_id_type or self.config.receive_id_type
        
        # Build message using proper JSON serialization (no manual escaping)
        message_card = {
            "config": {"wide_screen_mode": True},
            "header": {
                "title": {"tag": "plain_text", "content": title or "系统通知"},
                "template": "blue"
            },
            "elements": [
                {
                    "tag": "div",
                    "text": {"tag": "lark_md", "content": content}
                },
                {
                    "tag": "note",
                    "elements": [
                        {"tag": "plain_text", "content": f"发送时间: {time.strftime('%Y-%m-%d %H:%M:%S')}"}
                    ]
                }
            ]
        }
        
        payload = json.dumps({
            "receive_id": target_id,
            "msg_type": "interactive",
            "content": json.dumps(message_card)
        }).encode('utf-8')
        
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json"
        }
        
        url = f"{MESSAGE_ENDPOINT}?receive_id_type={target_type}"
        status, response = self._make_request(url, method="POST", headers=headers, data=payload)
        
        if status != 200 or response.get("code", -1) != 0:
            code = response.get("code", "unknown")
            msg = response.get("msg", "Unknown error")
            raise APIError(f"Failed to send message: {msg} (code: {code})", code, response)
        
        return {
            "success": True,
            "message_id": response.get("data", {}).get("message_id"),
            "create_time": response.get("data", {}).get("create_time")
        }


def main():
    """CLI entry point"""
    import argparse
    
    parser = argparse.ArgumentParser(
        description="Send notifications via Feishu (Lark)",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s -t "Alert" -m "Server CPU high"
  echo "Backup complete" | %(prog)s -t "Daily Backup"
  %(prog)s --test
        """
    )
    
    parser.add_argument("-t", "--title", default="系统通知", help="Message title")
    parser.add_argument("-m", "--message", help="Message content")
    parser.add_argument("-r", "--receive-id", help="Override receive ID")
    parser.add_argument("--receive-id-type", choices=["open_id", "user_id", "chat_id", "email"],
                       help="Override receive ID type")
    parser.add_argument("--test", action="store_true", help="Send test message")
    parser.add_argument("--config", action="store_true", help="Show current config (masked)")
    parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
    parser.add_argument("--json", action="store_true", help="Output raw JSON")
    parser.add_argument("--health", action="store_true", help="Health check (for watchdog)")
    
    args = parser.parse_args()
    
    # Load and validate config
    try:
        config = FeishuConfig().load()
        
        if args.config:
            print(json.dumps(config.to_safe_dict(), indent=2, ensure_ascii=False))
            sys.exit(0)
        
        config.validate()
        
    except ConfigError as e:
        result = {
            "success": False,
            "error": "CONFIG_ERROR",
            "message": str(e),
            "hint": "Set FEISHU_APP_ID, FEISHU_APP_SECRET, FEISHU_RECEIVE_ID environment variables"
        }
        print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(1)
    
    # Determine message content
    message = args.message
    if not message and not args.test:
        if not sys.stdin.isatty():
            message = sys.stdin.read()
        else:
            parser.print_help()
            sys.exit(1)
    
    # Send message
    try:
        notifier = FeishuNotifier(config)
        
        if args.health:
            # Health check for watchdog
            hostname = os.uname().nodename
            result = {
                "success": True,
                "status": "healthy",
                "hostname": hostname,
                "timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
            }
            print(json.dumps(result, indent=2, ensure_ascii=False))
            sys.exit(0)
        
        if args.test:
            hostname = os.uname().nodename
            title = "🧪 Feishu Notifier Test"
            content = f"**Server**: {hostname}  \n**Status**: Configuration OK  \n**Time**: {time.strftime('%Y-%m-%d %H:%M:%S')}"
        else:
            title = args.title
            content = message
        
        result = notifier.send_message(
            content=content,
            title=title,
            receive_id=args.receive_id,
            receive_id_type=args.receive_id_type
        )
        
        # Output result
        if args.json:
            print(json.dumps(result, indent=2, ensure_ascii=False))
        else:
            print(f"✓ Message sent successfully (ID: {result['message_id']})")
        
        sys.exit(0)
        
    except APIError as e:
        result = {
            "success": False,
            "error": "API_ERROR",
            "message": str(e),
            "code": e.code
        }
        print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(2)
        
    except Exception as e:
        result = {
            "success": False,
            "error": "UNEXPECTED_ERROR",
            "message": str(e)
        }
        print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(3)


if __name__ == "__main__":
    main()


path: run.sh
content: #!/bin/bash
#
# Feishu Notifier v2.0 - Entry Point
# 飞书通知技能入口脚本
#
# 这个脚本作为 OpenClaw skill 的入口，调用 Python 核心模块

set -e

# 获取脚本所在目录
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LIB_DIR="${SCRIPT_DIR}/lib"

# 设置 skill 目录环境变量（用于配置查找）
export FEISHU_SKILL_DIR="${SCRIPT_DIR}"

# 检查 Python 是否可用
if ! command -v python3 &> /dev/null; then
    echo '{"success":false,"error":"PYTHON_NOT_FOUND","message":"Python 3 is required but not installed"}' >&2
    exit 1
fi

# 执行 Python 核心模块
exec python3 "${LIB_DIR}/send.py" "$@"


path: scripts/install-discovery.sh
content: #!/bin/bash
#=============================================================================
# Auto-Discovery Installer
# Opt-in only: Scans for other skills' notify scripts
#=============================================================================

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
DISCOVERY_DIR="${SKILL_DIR}/.discovered"
OPENCLAW_DIR="${HOME}/.openclaw"

usage() {
    echo "Usage: $0 --install | --uninstall | --dry-run | --help"
    echo ""
    echo "Options:"
    echo "  --install     Install discovery links"
    echo "  --uninstall   Remove discovery links"
    echo "  --dry-run     Show what would be done"
    echo "  --help        Show this help"
    echo ""
    echo "This scans for notify scripts in known locations and creates links."
}

if [ $# -eq 0 ]; then
    usage
    exit 1
fi

ACTION=""
DRY_RUN=false

while [ $# -gt 0 ]; do
    case "$1" in
        --install) ACTION="install" ;;
        --uninstall) ACTION="uninstall" ;;
        --dry-run) DRY_RUN=true ;;
        --help) usage; exit 0 ;;
        *) echo "Unknown option: $1"; usage; exit 1 ;;
    esac
    shift
done

# Known locations to scan
SCAN_DIRS=(
    "${OPENCLAW_DIR}/workspace/*/scripts"
    "${OPENCLAW_DIR}/skills/*/scripts"
)

do_install() {
    echo "Scanning for notify scripts..."
    
    # Create discovery directory
    if [ "$DRY_RUN" = false ]; then
        mkdir -p "$DISCOVERY_DIR"
    fi
    
    found=0
    for pattern in "${SCAN_DIRS[@]}"; do
        for dir in $pattern; do
            if [ -d "$dir" ]; then
                for script in "$dir"/*notify*.sh "$dir"/*Notifier*.sh; do
                    if [ -f "$script" ] && [ -x "$script" ]; then
                        basename=$(basename "$script")
                        link="${DISCOVERY_DIR}/${basename}"
                        
                        if [ "$DRY_RUN" = true ]; then
                            echo "  [DRY RUN] Would link: $script -> $link"
                        else
                            if [ ! -L "$link" ]; then
                                ln -sf "$script" "$link"
                                echo "Linked: $basename"
                                found=1
                            fi
                        fi
                    fi
                done
            fi
        done
    done
    
    if [ "$found" = 0 ] && [ "$DRY_RUN" = false ]; then
        echo "No notify scripts found in scan locations"
    fi
    
    if [ "$DRY_RUN" = true ]; then
        echo "Scan complete (dry run)"
    else
        echo "Discovery links installed in: $DISCOVERY_DIR"
    fi
}

do_uninstall() {
    echo "Removing discovery links..."
    
    if [ "$DRY_RUN" = true ]; then
        if [ -d "$DISCOVERY_DIR" ]; then
            echo "  [DRY RUN] Would remove all links in: $DISCOVERY_DIR"
            ls -la "$DISCOVERY_DIR"
        else
            echo "  [DRY RUN] Discovery directory not found"
        fi
    else
        if [ -d "$DISCOVERY_DIR" ]; then
            rm -rf "$DISCOVERY_DIR"
            echo "Removed successfully"
        else
            echo "Discovery directory not found, nothing to remove"
        fi
    fi
}

case "$ACTION" in
    install) do_install ;;
    uninstall) do_uninstall ;;
    *) echo "Error: No action specified"; usage; exit 1 ;;
esac


path: skill.json
content: {
  "name": "feishu-relay",
  "version": "3.1.0",
  "description": "Minimal Feishu (Lark) notification bridge. Core only: send messages, queue, retry. No system modifications by default. Safe mode.",
  "author": "crayfish-ai",
  "license": "MIT",
  "homepage": "https://github.com/crayfish-ai/feishu-relay",
  "issues": "https://github.com/crayfish-ai/feishu-relay/issues",
  "entry": "run.sh",
  "config": {
    "appId": {
      "type": "string",
      "description": "Feishu App ID (cli_xxxxxxxx)",
      "required": true,
      "envVar": "FEISHU_APP_ID"
    },
    "appSecret": {
      "type": "string",
      "description": "Feishu App Secret",
      "required": true,
      "sensitive": true,
      "envVar": "FEISHU_APP_SECRET"
    },
    "receiveId": {
      "type": "string",
      "description": "Target user/chat ID",
      "required": true,
      "envVar": "FEISHU_RECEIVE_ID"
    },
    "receiveIdType": {
      "type": "string",
      "description": "ID type: open_id, user_id, chat_id, email",
      "default": "open_id",
      "enum": ["open_id", "user_id", "chat_id", "email"],
      "envVar": "FEISHU_RECEIVE_ID_TYPE"
    }
  },
  "permissions": {
    "description": "This skill requires network access to Feishu API and local file read/write for configuration only.",
    "network": ["open.feishu.cn"],
    "filesystem": ["skill directory", "config file"]
  },
  "capabilities": {
    "core": ["send_message", "json_output", "retry"],
    "opt_in": ["global_notify_link", "crontab", "systemd", "watchdog", "auto_discovery"]
  },
  "safety": {
    "no_system_modification": true,
    "no_root_required": true,
    "no_auto_migration": true,
    "no_env_injection": true
  },
  "documentation": {
    "readme": "README.md",
    "skill": "SKILL.md",
    "advanced": "docs/advanced.md",
    "production": "docs/production.md",
    "security": "docs/security.md",
    "uninstall": "docs/uninstall.md"
  }
}


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