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+
Scan to join WeChat group