Back to skills
extension
Category: OtherNo API key required

qclaw-agent-audit

Audit and inspect QClaw agent configuration, skills, permissions, and environment. Use when the user wants to check their local QClaw setup, list agents, analyze skill assignments, verify permissions, or generate a comprehensive report of their QClaw environment for planning or troubleshooting.

personAuthor: user_5c543922hubcommunity

QClaw Agent Audit

Audit local QClaw configuration and generate a comprehensive report of agents, skills, permissions, and environment.

When to Use

  • Check how many agents are configured
  • List all available skills (built-in + user)
  • Analyze agent permissions and visibility
  • Verify model configuration
  • Generate reports for role/task planning
  • Share configuration with others for debugging

Quick Start

Run the audit by executing the Python code below with the exec tool:

#!/usr/bin/env python3
"""
QClaw Agent Audit - Scan and report QClaw configuration
"""

import json
import os
import platform
import sys
import argparse
from pathlib import Path
from datetime import datetime


def detect_os():
    """Detect operating system"""
    system = platform.system()
    if system == "Windows":
        return "Windows", os.environ.get("USERPROFILE", "")
    elif system == "Darwin":
        return "macOS", os.path.expanduser("~")
    else:
        return "Linux", os.path.expanduser("~")


def find_config_path(home_dir):
    """Find QClaw config file"""
    paths = [
        Path(home_dir) / ".qclaw" / "openclaw.json",
        Path(home_dir) / ".openclaw" / "openclaw.json",
        Path(home_dir) / ".config" / "qclaw" / "openclaw.json",
    ]
    
    # Windows: also check Program Files
    if platform.system() == "Windows":
        paths.append(Path("C:/Program Files/QClaw/resources/openclaw/config/openclaw.json"))
    
    for path in paths:
        if path.exists():
            return str(path)
    
    return None


def find_skill_dirs(home_dir):
    """Find skill directories"""
    dirs = []
    
    # Built-in skills
    builtin_paths = [
        Path("C:/Program Files/QClaw/resources/openclaw/config/skills"),
        Path("/usr/share/openclaw/skills"),
        Path("/opt/openclaw/skills"),
    ]
    
    for path in builtin_paths:
        if path.exists():
            dirs.append(("built-in", str(path)))
            break
    
    # User skills
    user_paths = [
        Path(home_dir) / ".qclaw" / "skills",
        Path(home_dir) / ".openclaw" / "workspace" / "skills",
    ]
    
    for path in user_paths:
        if path.exists():
            dirs.append(("user", str(path)))
    
    return dirs


def parse_skill_md(skill_path):
    """Parse SKILL.md frontmatter"""
    skill_file = Path(skill_path) / "SKILL.md"
    if not skill_file.exists():
        return None, None
    
    try:
        content = skill_file.read_text(encoding="utf-8")
        if content.startswith("---"):
            parts = content.split("---", 2)
            if len(parts) >= 3:
                frontmatter = parts[1].strip()
                name = None
                description = None
                for line in frontmatter.split("\n"):
                    if line.startswith("name:"):
                        name = line.split(":", 1)[1].strip()
                    elif line.startswith("description:"):
                        description = line.split(":", 1)[1].strip()
                return name, description
    except Exception:
        pass
    
    return None, None


def scan_skills(skill_dirs):
    """Scan all skills"""
    skills = {"built-in": [], "user": []}
    
    for skill_type, dir_path in skill_dirs:
        path = Path(dir_path)
        if not path.exists():
            continue
        
        for item in path.iterdir():
            if item.is_dir() and not item.name.startswith("."):
                name, description = parse_skill_md(item)
                skills[skill_type].append({
                    "id": item.name,
                    "name": name or item.name,
                    "description": description or "",
                    "path": str(item),
                })
    
    return skills


def read_agent_personality(workspace_path):
    """Read agent personality from SOUL.md and IDENTITY.md"""
    personality = {
        "name": None,
        "emoji": None,
        "vibe": None,
        "background": None,
        "style": None,
    }
    
    # Read IDENTITY.md
    identity_file = Path(workspace_path) / "IDENTITY.md"
    if identity_file.exists():
        try:
            content = identity_file.read_text(encoding="utf-8")
            for line in content.split("\n"):
                line = line.strip()
                if line.startswith("- Name:"):
                    personality["name"] = line.split(":", 1)[1].strip()
                elif line.startswith("- Emoji:"):
                    personality["emoji"] = line.split(":", 1)[1].strip()
                elif line.startswith("- Vibe:"):
                    personality["vibe"] = line.split(":", 1)[1].strip()
        except Exception:
            pass
    
    # Read SOUL.md
    soul_file = Path(workspace_path) / "SOUL.md"
    if soul_file.exists():
        try:
            content = soul_file.read_text(encoding="utf-8")
            lines = content.split("\n")
            current_section = None
            section_content = []
            
            for line in lines:
                stripped = line.strip()
                if stripped.startswith("## "):
                    if current_section and section_content:
                        text = " ".join(section_content).strip()
                        if current_section == "经历":
                            personality["background"] = text
                        elif current_section == "风格":
                            personality["style"] = text
                    current_section = stripped[3:].strip()
                    section_content = []
                elif current_section and stripped:
                    section_content.append(stripped)
            
            # Don't forget the last section
            if current_section and section_content:
                text = " ".join(section_content).strip()
                if current_section == "经历":
                    personality["background"] = text
                elif current_section == "风格":
                    personality["style"] = text
        except Exception:
            pass
    
    return personality


def analyze_config(config_path):
    """Analyze QClaw configuration"""
    try:
        with open(config_path, "r", encoding="utf-8") as f:
            config = json.load(f)
    except Exception as e:
        return {"error": str(e)}
    
    result = {
        "meta": config.get("meta", {}),
        "agents": [],
        "permissions": {},
        "models": {},
        "skills_config": {},
    }
    
    # Agents
    agents_config = config.get("agents", {})
    defaults = agents_config.get("defaults", {})
    agent_list = agents_config.get("list", [])
    
    for agent in agent_list:
        workspace = agent.get("workspace", defaults.get("workspace", ""))
        personality = read_agent_personality(workspace) if workspace else {}
        
        # Use IDENTITY.md name if available, fallback to config name
        config_name = agent.get("name", "Unnamed")
        identity_name = personality.get("name")
        display_name = identity_name if identity_name else config_name
        
        # Track if names differ (for reporting)
        if identity_name and identity_name != config_name:
            personality["config_name"] = config_name
        
        agent_info = {
            "id": agent.get("id", "unknown"),
            "name": display_name,
            "type": "main" if agent.get("id") == "main" else "subagent",
            "workspace": workspace,
            "skills": agent.get("skills", []),
            "model": agent.get("model", defaults.get("model", {})),
            "personality": personality,
        }
        result["agents"].append(agent_info)
    
    # Permissions
    tools = config.get("tools", {})
    result["permissions"] = {
        "session_visibility": tools.get("sessions", {}).get("visibility", "unknown"),
        "agent_to_agent": tools.get("agentToAgent", {}).get("enabled", False),
        "subagent_allowlist": agents_config.get("defaults", {}).get("subagents", {}).get("allowAgents", []),
    }
    
    # Models
    models = config.get("models", {})
    providers = models.get("providers", {})
    result["models"] = {
        "mode": models.get("mode", "unknown"),
        "primary": defaults.get("model", {}).get("primary", "unknown"),
        "fallbacks": defaults.get("model", {}).get("fallbacks", []),
        "providers": list(providers.keys()),
    }
    
    # Skills config
    skills_entries = config.get("skills", {}).get("entries", {})
    result["skills_config"] = {
        k: v.get("enabled", False) if isinstance(v, dict) else bool(v)
        for k, v in skills_entries.items()
    }
    
    return result


def generate_markdown_report(data, skills, output_path):
    """Generate Markdown report"""
    lines = []
    lines.append("# QClaw Agent Audit Report")
    lines.append(f"\n**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    lines.append(f"**OS:** {data.get('os', 'Unknown')}")
    lines.append(f"**QClaw Version:** {data.get('meta', {}).get('lastTouchedVersion', 'Unknown')}")
    lines.append("")
    
    # Agents
    lines.append("## Agents")
    lines.append("")
    lines.append("| ID | Name | Type | Skills |")
    lines.append("|----|------|------|--------|")
    for agent in data.get("agents", []):
        skill_count = len(agent.get("skills", []))
        skills_str = f"{skill_count} explicit" if skill_count > 0 else "inherits defaults"
        lines.append(f"| {agent['id']} | {agent['name']} | {agent['type']} | {skills_str} |")
    lines.append("")
    
    # Agent details
    for agent in data.get("agents", []):
        personality = agent.get("personality", {})
        lines.append(f"### {agent['name']} ({agent['id']})")
        lines.append(f"- **Type:** {agent['type']}")
        lines.append(f"- **Workspace:** {agent.get('workspace', 'N/A')}")
        
        # Show config name if different from identity name
        if personality.get("config_name"):
            lines.append(f"- **Config Name:** {personality['config_name']} *(IDENTITY.md overrides this)*")
        
        # Personality
        if personality.get("emoji"):
            lines.append(f"- **Emoji:** {personality['emoji']}")
        if personality.get("vibe"):
            lines.append(f"- **Vibe:** {personality['vibe']}")
        if personality.get("background"):
            lines.append(f"- **Background:** {personality['background']}")
        if personality.get("style"):
            lines.append(f"- **Style:** {personality['style']}")
        
        if agent.get("skills"):
            lines.append(f"- **Explicit Skills:** {', '.join(agent['skills'])}")
        lines.append("")
    
    # Permissions
    perms = data.get("permissions", {})
    lines.append("## Permissions")
    lines.append(f"- **Session Visibility:** {perms.get('session_visibility', 'unknown')}")
    lines.append(f"- **Agent-to-Agent Communication:** {'Enabled' if perms.get('agent_to_agent') else 'Disabled'}")
    lines.append(f"- **Subagent Allowlist:** {', '.join(perms.get('subagent_allowlist', []))}")
    lines.append("")
    
    # Models
    models = data.get("models", {})
    lines.append("## Models")
    lines.append(f"- **Primary:** {models.get('primary', 'unknown')}")
    lines.append(f"- **Fallbacks:** {', '.join(models.get('fallbacks', []))}")
    lines.append(f"- **Providers:** {', '.join(models.get('providers', []))}")
    lines.append("")
    
    # Skills
    lines.append("## Skills")
    lines.append("")
    
    # Built-in
    lines.append(f"### Built-in Skills ({len(skills.get('built-in', []))})")
    lines.append("")
    for skill in sorted(skills.get("built-in", []), key=lambda x: x["id"]):
        desc = skill.get("description", "")[:80] + "..." if len(skill.get("description", "")) > 80 else skill.get("description", "")
        lines.append(f"- **{skill['id']}**: {skill['name']}" + (f" - {desc}" if desc else ""))
    lines.append("")
    
    # User
    lines.append(f"### User Skills ({len(skills.get('user', []))})")
    lines.append("")
    lines.append("| Skill | Name | Purpose |")
    lines.append("|-------|------|---------|")
    for skill in sorted(skills.get("user", []), key=lambda x: x["id"]):
        desc = skill.get("description", "")
        # Clean up description for table
        desc = desc.replace("|", "/").replace("\n", " ")
        if len(desc) > 100:
            desc = desc[:97] + "..."
        lines.append(f"| {skill['id']} | {skill['name']} | {desc} |")
    lines.append("")
    
    # Warnings
    lines.append("## Notes")
    lines.append("- Subagents cannot modify system configuration (no gateway access)")
    lines.append("- All agents share built-in skills automatically")
    lines.append("- User skills must be explicitly assigned per agent")
    lines.append("")
    
    with open(output_path, "w", encoding="utf-8") as f:
        f.write("\n".join(lines))
    
    return output_path


def generate_json_report(data, skills, output_path):
    """Generate JSON report"""
    report = {
        "generated_at": datetime.now().isoformat(),
        "environment": {
            "os": data.get("os"),
            "python_version": platform.python_version(),
            "qclaw_version": data.get("meta", {}).get("lastTouchedVersion"),
        },
        "agents": data.get("agents", []),
        "permissions": data.get("permissions", {}),
        "models": data.get("models", {}),
        "skills": skills,
    }
    
    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(report, f, indent=2, ensure_ascii=False)
    
    return output_path


def main():
    parser = argparse.ArgumentParser(description="QClaw Agent Audit")
    parser.add_argument("--format", choices=["md", "json"], default="md", help="Output format")
    parser.add_argument("--config", help="Path to openclaw.json")
    parser.add_argument("--output", help="Output file path")
    args = parser.parse_args()
    
    # Detect environment
    os_name, home_dir = detect_os()
    print(f"OS: {os_name}")
    print(f"Home: {home_dir}")
    
    # Find config
    config_path = args.config or find_config_path(home_dir)
    if not config_path:
        print("Error: Could not find openclaw.json")
        sys.exit(1)
    
    print(f"Config: {config_path}")
    
    # Analyze config
    data = analyze_config(config_path)
    data["os"] = os_name
    
    if "error" in data:
        print(f"Error reading config: {data['error']}")
        sys.exit(1)
    
    # Scan skills
    skill_dirs = find_skill_dirs(home_dir)
    skills = scan_skills(skill_dirs)
    
    print(f"Built-in skills: {len(skills['built-in'])}")
    print(f"User skills: {len(skills['user'])}")
    
    # Generate report
    if args.format == "json":
        output = args.output or "qclaw-agent-audit-report.json"
        generate_json_report(data, skills, output)
    else:
        output = args.output or "qclaw-agent-audit-report.md"
        generate_markdown_report(data, skills, output)
    
    print(f"Report saved: {output}")


if __name__ == "__main__":
    main()

执行方式:将上述代码保存为临时文件后用 Python 运行,或直接用 exec 工具执行 Python 代码。

输出:Markdown 报告保存到 qclaw-agent-audit-report.md

What It Checks

Environment

  • Operating system
  • QClaw installation path
  • Python version
  • Config file location
  • Disk space

Agents

  • Agent ID and name
  • Type (main/subagent)
  • Workspace path
  • Explicit skill assignments

Skills

  • Built-in skills (from bundled directory)
  • User skills (from user directories)
  • Skill descriptions (from SKILL.md frontmatter)
  • Per-agent skill assignments

Permissions

  • Session visibility (all/self/none)
  • Agent-to-agent communication
  • Subagent delegation allowlist
  • Gateway access (config modification rights)

Models

  • Primary model
  • Fallback models
  • Available providers
  • Model capabilities (text/image)

Cross-Platform Support

| OS | Config Path Detection | |----|----------------------| | Windows | %USERPROFILE%\.qclaw\openclaw.json | | macOS | ~/.qclaw/openclaw.json | | Linux | ~/.qclaw/openclaw.json or $XDG_CONFIG_HOME/qclaw/openclaw.json |

Output Formats

Default: Markdown (--format md) Optional: JSON (--format json)

Notes

  • Read-only: Does not modify any configuration
  • Safe to run: Only reads config files and scans directories
  • Requires Python 3.7+