Customization Guide¶
How to extend and customize every subsystem in claude-superpowers. This guide covers custom skills, workflows, cron jobs, channel adapters, file watchers, notification profiles, dashboard extensions, CLI commands, environment overrides, and the template system.
Philosophy¶
claude-superpowers is designed around three principles that shape every customization point:
-
Local-first: Everything runs on your hardware. No cloud dependencies, no external SaaS requirements. Customizations should follow the same rule -- if an external service is unavailable, features degrade gracefully.
-
Convention over configuration: Subsystems discover resources by scanning well-known directories (
skills/,workflows/,~/.claude-superpowers/). Drop a file in the right place with the right shape and it works. -
Composable primitives: Skills, workflows, cron jobs, watchers, and messaging channels are independent units that connect through well-defined interfaces. A skill can be invoked from the CLI, a cron job, a workflow step, a file watcher, or an MCP tool -- the skill does not need to know which one called it.
Table of Contents¶
- Custom Skills
- Custom Workflows
- Custom Cron Jobs
- Custom Channel Adapters
- Custom File Watchers
- Notification Profiles
- Dashboard Customization
- CLI Extensions
- Environment and Config Overrides
- Template System
- Examples
Custom Skills¶
Skills are the primary unit of automation. Each skill is a self-contained directory under skills/ containing a manifest, an executable script, and optionally a slash command definition.
Creating a Skill from Scratch¶
The fastest path is the interactive scaffolder:
You are prompted for a name, description, and script type (bash or python). The scaffolder generates three files and registers the slash command immediately.
For non-interactive creation, pass everything as flags:
claw skill create \
--name cert-renewer \
--description "Renew Let's Encrypt certificates and reload nginx" \
--type bash \
--permission vault \
--permission ssh \
--trigger "cron:weekly"
To create a skill entirely by hand:
Create skills/cert-renewer/skill.yaml:
name: cert-renewer
version: "0.1.0"
description: "Renew Let's Encrypt certificates and reload nginx"
author: DreDay
script: run.sh
slash_command: true
dependencies: [certbot, ssh]
permissions: [vault, ssh]
triggers: ["cron:weekly"]
Create skills/cert-renewer/run.sh:
#!/usr/bin/env bash
set -euo pipefail
# cert-renewer -- Renew Let's Encrypt certificates and reload nginx
main() {
echo "[cert-renewer] checking certificate status..."
certbot renew --dry-run 2>&1
echo "[cert-renewer] reloading nginx..."
ssh web-server "sudo systemctl reload nginx"
echo "[cert-renewer] done"
}
main "$@"
Make it executable and validate:
skill.yaml Schema Reference¶
| Field | Required | Default | Type | Description |
|---|---|---|---|---|
name |
yes | -- | string | Kebab-case identifier, unique across all skills |
version |
yes | -- | string | Semantic version (e.g., "0.1.0") |
description |
yes | -- | string | One-line summary shown in claw skill list |
author |
yes | -- | string | Author attribution |
script |
yes | -- | string | Entry point script relative to skill directory |
slash_command |
no | false |
boolean | If true, claw skill sync creates a Claude Code slash command |
triggers |
no | [] |
list[string] | Event triggers for cron integration (e.g., cron:daily) |
dependencies |
no | [] |
list[string] | Binaries checked via which before execution |
permissions |
no | [] |
list[string] | Permission scopes for sandboxed execution |
skill_type |
no | "" |
string | Optional classification tag |
Script Types¶
The skill loader detects how to execute based on the script file extension:
| Extension | Execution Command |
|---|---|
.py |
python3 <script> |
.sh |
bash <script> |
| other | ./<script> (direct execution, must have shebang) |
Generated Templates¶
When claw skill create runs, it produces one of two templates:
Bash template -- includes a usage() function, set -euo pipefail for safety, argument parsing via $1, and a main function:
#!/usr/bin/env bash
set -euo pipefail
# my-skill -- Does something useful
usage() {
echo "Usage: $(basename "$0") [options]"
echo ""
echo " Does something useful"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
exit 0
}
[[ "${1:-}" == "-h" || "${1:-}" == "--help" ]] && usage
main() {
echo "[my-skill] running..."
# TODO: implement skill logic
}
main "$@"
Python template -- includes argparse, a main() returning an exit code, and __future__ annotations:
#!/usr/bin/env python3
"""my-skill -- Does something useful"""
from __future__ import annotations
import argparse
import sys
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Does something useful")
return parser.parse_args(argv)
def main(argv: list[str] | None = None) -> int:
args = parse_args(argv)
print(f"[my-skill] running...")
return 0
if __name__ == "__main__":
sys.exit(main())
Scaffolding from an Existing Script¶
If you already have a working script, wrap it as a skill in one step:
from superpowers.skill_creator import scaffold_from_existing
skill_dir = scaffold_from_existing(
source_script=Path("/home/ray/scripts/backup-check.sh"),
name="backup-check",
description="Verify ZFS snapshots on TrueNAS",
)
This copies your script into the skill directory, auto-detects the script type, generates skill.yaml and command.md, and preserves the original file unchanged.
Sandboxing and Permissions¶
Skills run in two modes depending on the caller:
Standard mode (SkillLoader.run()):
- Inherits the full parent process environment
- Skill runs in its own directory as cwd
- 5-minute execution timeout
Sandboxed mode (SkillLoader.run_sandboxed()):
- Minimal environment: only PATH, HOME, LANG, TERM are passed
- Skills with vault in their permissions list receive the full environment
- Same 5-minute timeout
- Used by the intake pipeline for automatic skill execution
| Permission | Effect |
|---|---|
vault |
Full environment passthrough (including vault secrets) |
ssh |
Declares SSH access intent (documentation/future enforcement) |
nmap |
Declares network scanning capability (documentation/future enforcement) |
| (custom) | Allowed; custom strings are stored but not currently enforced |
Dependency Gating¶
The dependencies field in skill.yaml is checked before execution. Every listed binary is verified via which. If any dependency is missing, the skill fails immediately with a clear error -- no partial execution occurs.
Argument Passing¶
Arguments passed via CLI are converted to SKILL_ prefixed environment variables:
Inside the script:
Slash Command Registration¶
When slash_command: true is set in skill.yaml, claw skill sync does the following:
- Generates
.claude/commands/<name>.mdinside the skill directory - Creates a symlink at
~/.claude/commands/<name>.mdpointing to that file - Claude Code discovers the symlink and registers
/<name>as a slash command
Sync is idempotent. Run it any time you add, remove, or modify skills.
Auto-Install¶
The auto-install system (superpowers/auto_install.py) can create skills on demand from a description. It works in three stages:
- Check existing skills -- tokenizes the description and looks for keyword overlap with registered skills
- Match a built-in template -- five templates are bundled:
network-scan,disk-usage,git-stats,docker-health,log-search - Scaffold a generic skill -- if no template matches, creates a stub skill from the description
Built-in templates:
| Template | Description | Tags |
|---|---|---|
network-scan |
Scan local network using nmap or ping sweep | network, scan, hosts, nmap, ping |
disk-usage |
Report disk usage with high-usage alerts | disk, usage, storage, space, df |
git-stats |
Git repository statistics | git, stats, commits, contributors |
docker-health |
Docker container and image health check | docker, health, containers, images |
log-search |
Search system and application logs | log, search, grep, syslog, errors |
Skill Lifecycle¶
| Stage | Command | What Happens |
|---|---|---|
| Create | claw skill create |
Scaffold manifest + script + command.md |
| Validate | claw skill validate skills/my-skill |
Check manifest schema + script existence |
| Sync | claw skill sync |
Register slash commands as symlinks |
| List | claw skill list |
Show all discovered skills |
| Run | claw skill run my-skill |
Execute with dependency check |
| Uninstall | Remove the directory | rm -rf skills/my-skill |
Custom Workflows¶
Workflows are YAML-defined multi-step pipelines that chain together shell commands, Claude prompts, skills, HTTP requests, and human approval gates. They live in the workflows/ directory.
Writing a Workflow YAML¶
Create a file in workflows/:
# workflows/nightly-maintenance.yaml
name: nightly-maintenance
description: "Run backups, prune Docker images, check SSL certs, send report"
notify_profile: info
steps:
- name: backup-databases
type: shell
command: "pg_dump mydb | gzip > /backups/mydb-$(date +%Y%m%d).sql.gz"
on_failure: abort
timeout: 600
- name: prune-docker
type: shell
command: "docker system prune -af --volumes"
on_failure: continue
- name: check-certs
type: skill
command: ssl-cert-check
on_failure: continue
- name: summarize
type: claude_prompt
command: "Summarize tonight's maintenance: backup status, docker prune results, and SSL cert status."
on_failure: continue
rollback:
- name: notify-failure
type: shell
command: "echo 'Nightly maintenance failed' | mail -s 'ALERT' admin@example.com"
Workflow YAML Schema¶
Top-level fields:
| Field | Required | Default | Type | Description |
|---|---|---|---|---|
name |
yes | -- | string | Unique workflow identifier |
description |
yes | -- | string | Human-readable summary |
notify_profile |
no | "" |
string | Notification profile to use on completion |
steps |
yes | -- | list | Ordered list of step configurations |
rollback |
no | [] |
list | Steps to execute when on_failure: rollback triggers |
Step fields:
| Field | Required | Default | Type | Description |
|---|---|---|---|---|
name |
yes | -- | string | Step identifier (used in output and conditions) |
type |
yes | -- | enum | shell, claude_prompt, skill, http, approval_gate |
command |
yes | -- | string | Command, prompt, skill name, or URL depending on type |
on_failure |
no | abort |
enum | abort (stop), continue (proceed), rollback (run rollback steps) |
timeout |
no | 300 |
integer | Maximum seconds before step is killed |
condition |
no | "" |
string | previous.ok, previous.failed, always, or "" (always) |
args |
no | {} |
dict | Extra arguments (step-type specific) |
Step Types in Detail¶
shell -- Runs a command via subprocess.run(). Extra args are injected as WF_<KEY> environment variables.
- name: build
type: shell
command: "make build"
args:
target: production
parallel: "4"
# Available in script as $WF_TARGET and $WF_PARALLEL
claude_prompt -- Runs claude -p "<prompt>" --output-format text. The prompt text goes in the command field.
- name: analyze-logs
type: claude_prompt
command: "Read /var/log/syslog from the last hour and list any anomalies."
timeout: 120
skill -- Executes a registered skill by name. The args dict is passed to the skill loader.
http -- Makes an HTTP request. Default method is POST. Configure via args:
- name: health-check
type: http
command: "http://localhost:8100/health"
args:
method: GET
headers:
Authorization: "Bearer ${API_TOKEN}"
body:
check: full
approval_gate -- Pauses for human confirmation via stdin. Auto-approved in --dry-run mode.
- name: confirm-deploy
type: approval_gate
args:
prompt: "Tests passed. Deploy to production? [y/N] "
Conditions¶
Steps can be conditionally executed based on the previous step's result:
| Condition | Behavior |
|---|---|
previous.ok |
Run only if the previous step passed |
previous.failed |
Run only if the previous step failed |
always |
Run regardless of previous step status |
"" (empty/default) |
Always run |
Example -- send an alert only on failure:
- name: deploy
type: shell
command: "docker compose up -d"
on_failure: continue
- name: alert-on-failure
type: shell
command: "claw msg notify critical 'Deploy failed!'"
condition: previous.failed
Quality Gates and Rollback¶
When a step with on_failure: rollback fails, the engine immediately executes all steps in the rollback section, then stops. Rollback steps run unconditionally and in order.
steps:
- name: deploy
type: shell
command: "docker compose up -d --build"
on_failure: rollback
rollback:
- name: revert
type: shell
command: "docker compose down && git checkout HEAD~1 && docker compose up -d"
- name: notify
type: shell
command: "claw msg notify critical 'Deploy rolled back'"
Notifications¶
Set notify_profile to a profile name from ~/.claude-superpowers/profiles.yaml. After the workflow completes (success or failure), a summary message is sent via the messaging system:
CLI Reference¶
claw workflow list # List available workflows
claw workflow show <name> # Show steps in detail
claw workflow run <name> # Execute workflow
claw workflow run <name> --dry-run # Preview without executing
claw workflow validate <name> # Check for errors
claw workflow init # Install built-in templates
Custom Cron Jobs¶
The cron subsystem supports four job types, three schedule formats, per-job model overrides, and output routing to messaging channels.
Job Types¶
shell -- Run any command as a subprocess. The daemon's environment (including .env values) is inherited.
claw cron add backup-check \
--type shell \
--command "ssh truenas 'zpool status'" \
--schedule "daily at 09:00"
claude -- Launch a headless Claude session via claude -p. The --prompt flag provides the prompt text.
claw cron add daily-summary \
--type claude \
--prompt "Review today's cron output and summarize in 5 bullet points." \
--schedule "daily at 18:00"
webhook -- Send an HTTP POST to a URL with optional JSON body.
claw cron add slack-morning \
--type webhook \
--url "https://hooks.slack.com/services/T00/B00/xxx" \
--body '{"text":"Good morning. All systems operational."}' \
--schedule "0 8 * * 1-5"
skill -- Invoke a registered skill by name.
Schedule Syntax¶
| Format | Example | Description |
|---|---|---|
| Cron expression | "0 */6 * * *" |
Standard 5-field cron: minute hour day month weekday |
| Interval | "every 6h" |
Units: s, m, h, d. One unit at a time. |
| Daily-at | "daily at 09:00" |
Once per day at the specified HH:MM |
Cron expressions follow minute hour day month weekday. Interval strings accept every <N><unit> where unit is s (seconds), m (minutes), h (hours), or d (days).
Output Routing¶
Every job writes output to a structured log path:
Optionally route output to a messaging channel or notification profile:
# Direct channel routing
claw cron add health-check \
--type skill \
--skill heartbeat \
--schedule "every 30m" \
--output "slack:#alerts"
# Profile routing
claw cron add health-check \
--type skill \
--skill heartbeat \
--schedule "every 30m" \
--output critical
Output format: <channel>:<target> for direct routing, or <profile_name> for profile-based fan-out.
Messaging failures are silently caught -- they never break job execution.
Per-Job Model Overrides¶
Each cron job can override the LLM model used for claude-type jobs. This is useful for routing expensive analysis to a specific model while keeping lighter tasks on the default.
engine.add_job(
name="deep-analysis",
schedule="daily at 02:00",
job_type="claude",
command="Analyze all system logs for the past 24 hours...",
llm_model="claude-sonnet-4-20250514", # Override for this job
)
The model resolution order is:
1. Per-job llm_model field (if non-empty)
2. JOB_MODEL environment variable
3. Default: "claude"
The resolved model is set as the LLM_MODEL environment variable in the job subprocess.
Job Environment Variables¶
Shell-type jobs receive extra environment variables from the args dict, prefixed with JOB_:
engine.add_job(
name="scan",
schedule="every 6h",
job_type="shell",
command="nmap -sn $JOB_SUBNET",
args={"subnet": "192.168.30.0/24"},
)
Inside the shell, $JOB_SUBNET is 192.168.30.0/24.
Custom Channel Adapters¶
The messaging system uses a registry of channel adapters, each implementing a common interface. Adding a new channel requires three files.
The Channel Contract¶
Every channel adapter extends superpowers.channels.base.Channel:
from superpowers.channels.base import Channel, ChannelType, SendResult
class Channel:
"""Base class for messaging channel adapters."""
channel_type: ChannelType
def send(self, target: str, message: str) -> SendResult:
"""Send a message to the specified target.
Args:
target: Channel-specific destination (Slack channel, email address, etc.)
message: Message text to send.
Returns:
SendResult with ok=True on success, ok=False with error on failure.
"""
raise NotImplementedError
def test_connection(self) -> SendResult:
"""Verify credentials and connectivity.
Returns:
SendResult with ok=True if the adapter can send messages.
"""
raise NotImplementedError
The SendResult dataclass:
@dataclass
class SendResult:
ok: bool # Whether the send succeeded
channel: str # Channel name (e.g., "slack")
target: str # Where the message was sent
message: str = "" # Success details (e.g., message ID)
error: str = "" # Error description on failure
Adding a New Channel: Step by Step¶
1. Define the ChannelType enum value.
Edit superpowers/channels/base.py and add your channel to the ChannelType enum:
class ChannelType(StrEnum):
slack = "slack"
telegram = "telegram"
discord = "discord"
email = "email"
imessage = "imessage"
matrix = "matrix" # New channel
2. Create the adapter module.
Create superpowers/channels/matrix.py:
"""Matrix channel adapter."""
from __future__ import annotations
from superpowers.channels.base import Channel, ChannelError, ChannelType, SendResult
class MatrixChannel(Channel):
channel_type = ChannelType.matrix
def __init__(self, homeserver: str, access_token: str):
if not homeserver or not access_token:
raise ChannelError("Matrix homeserver URL and access token are required")
self._homeserver = homeserver.rstrip("/")
self._token = access_token
def send(self, target: str, message: str) -> SendResult:
"""Send a message to a Matrix room.
Args:
target: Room ID (e.g., "!abc123:matrix.org")
message: Plain text message.
"""
import json
import urllib.request
url = f"{self._homeserver}/_matrix/client/r0/rooms/{target}/send/m.room.message"
data = json.dumps({"msgtype": "m.text", "body": message}).encode()
req = urllib.request.Request(
url, data=data,
headers={
"Authorization": f"Bearer {self._token}",
"Content-Type": "application/json",
},
method="PUT",
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
return SendResult(ok=True, channel="matrix", target=target)
except Exception as exc:
return SendResult(
ok=False, channel="matrix", target=target, error=str(exc),
)
def test_connection(self) -> SendResult:
import urllib.request
url = f"{self._homeserver}/_matrix/client/r0/account/whoami"
req = urllib.request.Request(
url, headers={"Authorization": f"Bearer {self._token}"},
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
return SendResult(ok=True, channel="matrix", target="")
except Exception as exc:
return SendResult(
ok=False, channel="matrix", target="", error=str(exc),
)
3. Register the adapter in the channel registry.
Edit superpowers/channels/registry.py:
Add availability detection in the available() method:
def available(self) -> list[str]:
names = []
# ... existing channels ...
if s.matrix_homeserver and s.matrix_access_token:
names.append("matrix")
return names
Add the factory case in _create():
def _create(self, name: str) -> Channel:
# ... existing channels ...
elif name == ChannelType.matrix.value:
from superpowers.channels.matrix import MatrixChannel
return MatrixChannel(
homeserver=s.matrix_homeserver,
access_token=s.matrix_access_token,
)
else:
raise ChannelError(f"Unknown channel: {name}")
4. Add settings fields.
Edit superpowers/config.py and add the credential fields to the Settings dataclass:
Add the _env() calls in Settings.load():
5. Add environment variables to .env.example:
The new channel is now available in all messaging commands:
It can also be used in notification profiles and cron output routing.
Inbound Channel Adapters (Phase G)¶
For bidirectional channels that receive messages (webhooks, bot polling), an additional abstract base class is defined in msg_gateway/channels/base.py:
| Method | Description |
|---|---|
receive(request) -> Message |
Parse inbound webhook payload |
acknowledge(message) -> None |
Send read receipt or reaction |
start_processing_indicator(message) -> None |
Show typing indicator |
send_response(message, response) -> None |
Send reply |
supports_streaming: bool |
Whether the adapter supports streaming responses |
This is separate from the simpler outbound-only Channel class. Existing adapters can migrate to this interface incrementally.
Custom File Watchers¶
File watchers monitor directories for changes and trigger actions automatically. Rules are defined in ~/.claude-superpowers/watchers.yaml.
Watcher Rule Schema¶
- name: screenshot-optimizer # Unique rule ID (required)
path: ~/Desktop/Screenshot*.png # Directory or glob pattern (required)
events: [created] # Event types (default: [created])
action: shell # Action type (required)
command: "optipng $WATCHER_FILE" # Action target (required)
args: {} # Extra arguments (default: {})
enabled: true # Active flag (default: true)
| Field | Required | Default | Type | Description |
|---|---|---|---|---|
name |
yes | -- | string | Unique rule identifier |
path |
yes | -- | string | Directory or glob pattern to monitor |
events |
no | [created] |
list[enum] | created, modified, deleted, moved |
action |
yes | -- | enum | shell, skill, workflow, move, copy |
command |
yes | -- | string | Command to run or target path |
args |
no | {} |
dict | Extra arguments |
enabled |
no | true |
boolean | Whether the rule is active |
Action Types¶
| Action | command Value |
Behavior |
|---|---|---|
shell |
Shell command | Runs command with WATCHER_FILE set to triggering path. Extra args are available as WATCHER_{KEY} env vars. |
skill |
Skill name | Runs the named skill with file argument set to the triggering path. |
workflow |
Workflow name | Triggers the named workflow. |
move |
Target directory | Moves the triggering file to the specified directory. |
copy |
Target directory | Copies the triggering file to the specified directory. |
Event Types¶
| Event | Trigger |
|---|---|
created |
A new file appears in the watched directory |
modified |
An existing file's contents change |
deleted |
A file is removed |
moved |
A file is renamed or moved within/to the directory |
Practical Watcher Examples¶
Auto-optimize screenshots:
- name: screenshot-optimizer
path: ~/Desktop/Screenshot*.png
events: [created]
action: shell
command: "optipng $WATCHER_FILE && notify-send 'Optimized' \"$(basename $WATCHER_FILE)\""
Process invoices with a skill:
- name: invoice-processor
path: ~/Documents/invoices/*.pdf
events: [created]
action: skill
command: process-invoice
Backup config changes:
- name: config-backup
path: /etc/nginx/conf.d/*.conf
events: [modified]
action: copy
command: /backups/nginx-configs/
Trigger a deploy workflow when a release tag appears:
- name: release-trigger
path: ~/releases/*.tar.gz
events: [created]
action: workflow
command: deploy
Managing Watchers¶
claw watcher list # List configured rules and status
claw watcher start # Start the watcher daemon (foreground)
claw watcher test <rule-name> # Simulate a created event for testing
The watcher daemon logs to ~/.claude-superpowers/logs/watcher-daemon.log.
Notification Profiles¶
Profiles map a name to one or more channel+target pairs, enabling fan-out messaging with a single command.
Creating a Profile¶
Edit ~/.claude-superpowers/profiles.yaml:
critical:
- channel: slack
target: "#alerts"
- channel: telegram
target: "123456789"
info:
- channel: slack
target: "#general"
daily-digest:
- channel: email
target: admin@example.com
- channel: slack
target: "#daily"
on-call:
- channel: telegram
target: "987654321"
- channel: email
target: oncall@example.com
Profile Entry Fields¶
| Field | Type | Description |
|---|---|---|
channel |
enum | slack, telegram, discord, email (or any custom adapter) |
target |
string | Channel-specific destination: Slack channel name, Telegram chat ID, Discord channel ID, email address |
Using Profiles¶
From the CLI:
From a cron job (output routing):
claw cron add health-check \
--type skill --skill heartbeat \
--schedule "every 30m" \
--output critical
From a workflow (completion notification):
Programmatically:
from superpowers.channels.registry import ChannelRegistry
from superpowers.config import Settings
from superpowers.profiles import ProfileManager
settings = Settings.load()
registry = ChannelRegistry(settings)
pm = ProfileManager(registry)
results = pm.send("critical", "Database backup failed!")
for r in results:
print(f"{r.channel}: {'OK' if r.ok else r.error}")
Listing Profiles¶
Displays all defined profiles with their channel and target mappings.
Dashboard Customization¶
The dashboard is a FastAPI application with Alpine.js + htmx on the frontend. It is structured around routers, each handling a specific subsystem.
Architecture¶
dashboard/
app.py # FastAPI app, router registration, static mount
deps.py # Dependency injection (auth, settings)
middleware.py # Rate limiting middleware
routers/ # One module per subsystem
status.py # /api/status
cron.py # /api/cron/*
messaging.py # /api/msg/*
skills.py # /api/skills/*
workflows.py # /api/workflows/*
memory.py # /api/memory/*
ssh.py # /api/ssh/*
audit.py # /api/audit/*
vault.py # /api/vault/*
watchers.py # /api/watchers/*
browser.py # /api/browser/*
chat.py # /api/chat/*
notifications.py # /api/notifications/*
jobs.py # /api/jobs/*
settings.py # /api/settings/*
auth.py # /auth/* (public, no auth required)
static/ # Alpine.js + htmx SPA
Adding a New API Router¶
1. Create the router module.
Create dashboard/routers/my_feature.py:
from fastapi import APIRouter
router = APIRouter()
@router.get("/")
def list_items():
"""List all items."""
return {"items": []}
@router.post("/")
def create_item(name: str):
"""Create a new item."""
return {"name": name, "status": "created"}
@router.get("/{item_id}")
def get_item(item_id: str):
"""Get a specific item."""
return {"id": item_id}
2. Register the router in app.py.
Import and include the router in the protected API group:
from dashboard.routers import my_feature
api_router.include_router(
my_feature.router,
prefix="/my-feature",
tags=["my-feature"],
)
All routes under api_router are automatically protected by HTTP Basic auth.
3. Add a frontend page (optional).
Add an HTML page to dashboard/static/ that uses htmx or Alpine.js to interact with your API endpoints:
<div x-data="{ items: [] }" x-init="
fetch('/api/my-feature/', { headers: { 'Authorization': 'Basic ' + btoa(user + ':' + pass) } })
.then(r => r.json())
.then(data => items = data.items)
">
<template x-for="item in items">
<div x-text="item.name"></div>
</template>
</div>
Existing API Endpoints¶
The dashboard exposes 44 REST endpoints across 15 routers. All /api/* endpoints require HTTP Basic authentication. The /health endpoint is public.
| Router | Prefix | Purpose |
|---|---|---|
status |
/api |
System status overview |
cron |
/api/cron |
Cron job management |
messaging |
/api/msg |
Send messages, list channels |
skills |
/api/skills |
Skill listing and execution |
workflows |
/api/workflows |
Workflow listing and execution |
memory |
/api/memory |
Memory store CRUD |
ssh |
/api/ssh |
Remote command execution |
audit |
/api/audit |
Audit log search and tail |
vault |
/api/vault |
Credential management |
watchers |
/api/watchers |
File watcher management |
browser |
/api/browser |
Browser automation |
chat |
/api/chat |
Chat interface |
notifications |
/api/notifications |
Notification management |
jobs |
/api/jobs |
Job orchestration |
settings |
/api/settings |
Runtime configuration |
CLI Extensions¶
The claw CLI is built with Click 8.x. Each subsystem registers its commands through Click groups, and the main entry point (superpowers/cli.py) aggregates them all.
How the CLI Is Structured¶
# superpowers/cli.py
@click.group()
@click.version_option(version=__version__, prog_name="claw")
def main():
"""Claude Superpowers -- autonomous skill execution and orchestration."""
main.add_command(vault_group)
main.add_command(cron_group)
main.add_command(msg_group)
main.add_command(skill) # Invocable group with subcommands
main.add_command(workflow_group)
main.add_command(ssh_group)
main.add_command(browse_group)
main.add_command(memory_group)
main.add_command(watcher_group)
main.add_command(audit_group)
main.add_command(intake_group)
main.add_command(template_group)
main.add_command(setup_group)
main.add_command(jobs_group)
main.add_command(daemon)
main.add_command(dashboard_cmd)
main.add_command(status_dashboard)
Adding a New Subcommand¶
1. Create a CLI module.
Create superpowers/cli_myfeature.py:
"""Click subcommands for my feature."""
from __future__ import annotations
import click
from rich.console import Console
from rich.table import Table
console = Console()
@click.group("myfeature")
def myfeature_group():
"""Manage my custom feature."""
@myfeature_group.command("list")
def myfeature_list():
"""List all items."""
table = Table(title="My Feature Items")
table.add_column("Name", style="cyan")
table.add_column("Status")
# Add your items here
console.print(table)
@myfeature_group.command("run")
@click.argument("name")
@click.option("--dry-run", is_flag=True, help="Preview without executing")
def myfeature_run(name: str, dry_run: bool):
"""Execute a named item."""
if dry_run:
console.print(f"[dim]Would execute: {name}[/dim]")
return
console.print(f"[green]Executing:[/green] {name}")
# Implementation here
2. Register in cli.py.
3. Verify.
CLI Conventions¶
The project follows these patterns for CLI commands:
- Rich output: Use
rich.console.Consoleandrich.table.Tablefor formatted output - Click groups: Each subsystem is a
@click.group()with subcommands - Lazy imports: Import heavy modules inside command functions, not at module level
- Consistent naming: Command groups use
<subsystem>_groupnaming - Error handling: Use
raise SystemExit(1)for non-zero exits,click.echofor user-facing errors
Environment and Config Overrides¶
.env Variables¶
The .env file in the project root is loaded at startup by superpowers/config.py. Shell environment variables take precedence over .env values. The loader is a minimal built-in parser with no external dependency.
Full variable reference:
| Category | Variable | Default | Description |
|---|---|---|---|
| LLM | ANTHROPIC_API_KEY |
"" |
API key for claude-type jobs and intake |
| Messaging | SLACK_BOT_TOKEN |
"" |
Slack bot token (xoxb-...) |
| Messaging | TELEGRAM_BOT_TOKEN |
"" |
Telegram Bot API token |
| Messaging | TELEGRAM_DEFAULT_CHAT_ID |
"" |
Default Telegram chat ID |
| Messaging | DISCORD_BOT_TOKEN |
"" |
Discord bot token |
| Messaging | SMTP_HOST |
"" |
SMTP server hostname |
| Messaging | SMTP_USER |
"" |
SMTP login username |
| Messaging | SMTP_PASS |
"" |
SMTP login password |
| Messaging | SMTP_PORT |
587 |
SMTP port |
| Messaging | SMTP_FROM |
"" |
From address for outbound emails |
| Dashboard | DASHBOARD_USER |
"" |
HTTP Basic auth username (must be set) |
| Dashboard | DASHBOARD_PASS |
"" |
HTTP Basic auth password (must be set) |
| Dashboard | DASHBOARD_SECRET |
"" |
JWT signing secret (auto-generated if empty) |
| Infra | REDIS_URL |
redis://localhost:6379/0 |
Redis connection URL |
| Vault | VAULT_IDENTITY_FILE |
~/.claude-superpowers/vault.key |
age identity file path |
| SSH | SSH_CONNECT_TIMEOUT |
10 |
SSH connection timeout (seconds) |
| SSH | SSH_COMMAND_TIMEOUT |
30 |
SSH command timeout (seconds) |
| Home | HOME_ASSISTANT_URL |
"" |
Home Assistant base URL |
| Home | HOME_ASSISTANT_TOKEN |
"" |
Home Assistant access token |
| Model | CHAT_MODEL |
claude |
Model for interactive chat |
| Model | JOB_MODEL |
claude |
Model for background jobs |
| Telegram | ALLOWED_CHAT_IDS |
"" |
Comma-separated allowlist (empty = all rejected) |
| Telegram | TELEGRAM_SESSION_TTL |
3600 |
Session history TTL (seconds) |
| Telegram | TELEGRAM_MAX_HISTORY |
20 |
Max messages per session |
| Telegram | TELEGRAM_MAX_PER_CHAT |
2 |
Max concurrent jobs per chat |
| Telegram | TELEGRAM_MAX_GLOBAL |
5 |
Max concurrent jobs globally |
| Telegram | TELEGRAM_QUEUE_OVERFLOW |
10 |
Max queued jobs before rejecting |
| Telegram | TELEGRAM_MODE |
polling |
webhook or polling |
| Telegram | TELEGRAM_WEBHOOK_SECRET |
"" |
Secret for webhook validation |
| Telegram | TELEGRAM_WEBHOOK_URL |
"" |
Public URL for webhook endpoint |
| Telegram | TELEGRAM_ADMIN_CHAT_ID |
"" |
Admin chat ID for access requests |
| Security | ENVIRONMENT |
development |
development or production |
| Security | FORCE_HTTPS |
false |
Enforce HTTPS transport |
| Security | WEBHOOK_REQUIRE_SIGNATURE |
true |
Fail-closed webhook validation |
| Security | RATE_LIMIT_PER_IP |
60 |
Max requests per minute per IP |
| Security | RATE_LIMIT_PER_USER |
120 |
Max requests per minute per user |
| Data | SUPERPOWERS_DATA_DIR |
~/.claude-superpowers |
Base data directory |
| Data | CLAUDE_SUPERPOWERS_DATA_DIR |
~/.claude-superpowers |
Legacy alias |
Configuration Files¶
All runtime configuration lives in ~/.claude-superpowers/ (overridable via SUPERPOWERS_DATA_DIR):
| File | Format | Purpose |
|---|---|---|
hosts.yaml |
YAML | SSH host definitions |
profiles.yaml |
YAML | Notification profiles |
watchers.yaml |
YAML | File watcher rules |
rotation_policies.yaml |
YAML | Credential rotation policies |
templates.json |
JSON | Template manager manifest |
cron/jobs.json |
JSON | Cron job manifest |
cron/scheduler.db |
SQLite | APScheduler state |
memory.db |
SQLite | Persistent memory store |
vault.enc |
Binary | age-encrypted credentials |
age-identity.txt |
Text | age private key (chmod 600) |
audit.log |
JSONL | Append-only audit log |
Data Directory Override¶
To relocate all runtime data:
export SUPERPOWERS_DATA_DIR=/data/claude-superpowers
claw vault init # Creates the directory structure
The Settings.ensure_dirs() method creates all required subdirectories:
<data_dir>/
skills/
cron/
vault/
logs/
msg/
ssh/
watcher/
browser/
browser/profiles/
memory/
workflows/
Runtime Settings Access¶
Load settings programmatically:
from superpowers.config import Settings
settings = Settings.load()
print(settings.redis_url) # "redis://localhost:6379/0"
print(settings.data_dir) # Path("~/.claude-superpowers")
print(settings.telegram_bot_token) # Value from .env or environment
Pass a custom .env path:
Run security validation at startup:
warnings = settings.validate_security()
# Returns list of strings; each string is a security concern
Template System¶
The template manager tracks shipped configuration files (workflow YAMLs, docker-compose files, .env.example), detects user modifications, and supports upgrade with backup. This prevents git pull from blindly overwriting customized config.
How It Works¶
Templates are tracked in a JSON manifest at ~/.claude-superpowers/templates.json. Each entry records:
- The template name
- The SHA-256 hash of the shipped version
- The SHA-256 hash of the installed version
- The destination path
- The installation timestamp
Managed Templates¶
| Template | Source File | Description |
|---|---|---|
docker-compose.yaml |
docker-compose.yaml |
Docker Compose stack definition |
docker-compose.prod.yaml |
docker-compose.prod.yaml |
Production Compose overrides |
workflows/deploy.yaml |
workflows/deploy.yaml |
Deploy workflow |
workflows/backup.yaml |
workflows/backup.yaml |
Backup workflow |
workflows/morning-brief.yaml |
workflows/morning-brief.yaml |
Morning briefing workflow |
.env.example |
.env.example |
Configuration template |
Template Operations¶
Initialize -- Copy templates that do not yet exist at their destination:
Only copies files that are missing. Existing files (even if modified) are left untouched.
List -- Show all tracked templates and their modification status:
Status values:
| Status | Meaning |
|---|---|
current |
File matches the shipped version exactly |
modified |
User has changed the file since installation |
missing |
File was deleted by the user |
untracked |
Template has not been initialized yet |
Diff -- Show differences between current files and shipped versions:
Output is in unified diff format.
Reset -- Restore a template to its shipped version. Creates a .bak backup of the current file:
claw template reset docker-compose.yaml
# Creates docker-compose.yaml.bak, then overwrites with shipped version
Upgrade -- Apply template updates from a new project version, preserving user customizations:
Upgrade behavior per template:
| Condition | Action |
|---|---|
| File is unmodified by user | Replace with new shipped version |
| File has been modified by user | Create timestamped backup, then replace |
| File was deleted by user | Skip (respects intentional removal) |
| Source file is missing | Skip with "missing_source" status |
Backup files are named <file>.<suffix>.<timestamp>.bak (e.g., docker-compose.yaml.20260303120000.bak).
Programmatic Access¶
from superpowers.template_manager import TemplateManager
tm = TemplateManager()
# Initialize templates
installed = tm.init()
# List with status
for t in tm.list_templates():
print(f"{t['name']}: {t['status']}")
# Check diffs
diffs = tm.diff("docker-compose.yaml")
if diffs["docker-compose.yaml"]:
print("File has been modified")
# Reset to shipped version
tm.reset("docker-compose.yaml")
# Upgrade all templates
actions = tm.upgrade()
for name, action in actions.items():
print(f"{name}: {action}")
Custom Template Sources¶
Override the default template sources when constructing the manager:
tm = TemplateManager(
project_dir=Path("/home/ray/claude-superpowers"),
template_sources={
"my-config.yaml": "deploy/my-config.yaml",
"custom-workflow.yaml": "workflows/custom-workflow.yaml",
},
)
Examples¶
Example 1: SSL Certificate Monitor with Alerts¶
Create a skill that checks SSL certificate expiration across your domains and sends alerts through a notification profile.
1. Create the skill:
skills/ssl-monitor/skill.yaml:
name: ssl-monitor
version: "0.1.0"
description: "Check SSL certificate expiration and alert on upcoming renewals"
author: DreDay
script: run.sh
slash_command: true
dependencies: [openssl]
permissions: []
triggers: []
skills/ssl-monitor/run.sh:
#!/usr/bin/env bash
set -euo pipefail
DOMAINS="${SKILL_DOMAINS:-example.com,api.example.com,app.example.com}"
WARN_DAYS="${SKILL_WARN_DAYS:-30}"
EXIT_CODE=0
echo "[ssl-monitor] Checking certificates (warn < ${WARN_DAYS} days)"
echo ""
IFS=',' read -ra DOMAIN_LIST <<< "$DOMAINS"
for domain in "${DOMAIN_LIST[@]}"; do
expiry=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$expiry" ]; then
echo "FAIL $domain -- could not retrieve certificate"
EXIT_CODE=1
continue
fi
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$expiry" +%s 2>/dev/null)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt "$WARN_DAYS" ]; then
echo "WARN $domain -- expires in ${days_left} days ($expiry)"
EXIT_CODE=1
else
echo "OK $domain -- expires in ${days_left} days"
fi
done
exit $EXIT_CODE
2. Create a notification profile:
Add to ~/.claude-superpowers/profiles.yaml:
3. Schedule the check:
claw cron add ssl-check-daily \
--type skill \
--skill ssl-monitor \
--schedule "daily at 08:00" \
--output ssl-alerts
4. Test it:
Example 2: Deploy Workflow with Approval Gate and Rollback¶
Create a custom workflow that pulls code, runs tests, waits for manual approval, deploys, runs a health check, and rolls back on failure.
workflows/staging-deploy.yaml:
name: staging-deploy
description: "Deploy to staging with approval gate and automatic rollback"
notify_profile: critical
steps:
- name: git-pull
type: shell
command: "git -C /opt/myapp pull origin staging"
on_failure: abort
- name: install-deps
type: shell
command: "cd /opt/myapp && pip install -r requirements.txt"
on_failure: abort
timeout: 120
- name: run-tests
type: shell
command: "cd /opt/myapp && PYTHONPATH=. pytest tests/ -q --tb=short"
on_failure: abort
timeout: 300
- name: approve-deploy
type: approval_gate
args:
prompt: "Tests passed. Deploy to staging? [y/N] "
- name: docker-deploy
type: shell
command: "cd /opt/myapp && docker compose -f docker-compose.staging.yaml up -d --build"
on_failure: rollback
timeout: 180
- name: health-check
type: http
command: "http://staging.example.com/health"
args:
method: GET
on_failure: rollback
timeout: 30
- name: smoke-test
type: skill
command: qa-guardian
on_failure: rollback
rollback:
- name: revert-containers
type: shell
command: "cd /opt/myapp && docker compose -f docker-compose.staging.yaml down"
- name: revert-code
type: shell
command: "git -C /opt/myapp checkout HEAD~1"
- name: redeploy-previous
type: shell
command: "cd /opt/myapp && docker compose -f docker-compose.staging.yaml up -d"
- name: notify-rollback
type: shell
command: "claw msg notify critical 'Staging deploy rolled back to previous version'"
Test it first:
Then execute:
Example 3: Automated Log Ingestion Pipeline¶
Combine a file watcher, a custom skill, and a cron job to automatically process and summarize log files.
1. Create the log processor skill:
skills/log-ingest/skill.yaml:
name: log-ingest
version: "0.1.0"
description: "Parse and index a log file into the memory store"
author: DreDay
script: run.py
slash_command: false
dependencies: []
permissions: []
skills/log-ingest/run.py:
#!/usr/bin/env python3
"""log-ingest -- Parse a log file and store key events in memory."""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
def main() -> int:
log_file = os.environ.get("SKILL_FILE") or os.environ.get("WATCHER_FILE")
if not log_file:
print("[log-ingest] Error: no file specified")
return 1
path = Path(log_file)
if not path.exists():
print(f"[log-ingest] File not found: {path}")
return 1
errors = []
warnings = []
for line in path.read_text().splitlines():
if re.search(r"\bERROR\b", line, re.IGNORECASE):
errors.append(line.strip())
elif re.search(r"\bWARN(ING)?\b", line, re.IGNORECASE):
warnings.append(line.strip())
print(f"[log-ingest] Processed {path.name}")
print(f" Errors: {len(errors)}")
print(f" Warnings: {len(warnings)}")
if errors:
print("\n Top errors:")
for e in errors[:5]:
print(f" {e[:120]}")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())
2. Set up the file watcher:
Add to ~/.claude-superpowers/watchers.yaml:
- name: log-ingest
path: /var/log/myapp/*.log
events: [modified]
action: skill
command: log-ingest
enabled: true
3. Schedule a daily summary:
claw cron add log-summary \
--type claude \
--prompt "Review today's log-ingest output in ~/.claude-superpowers/cron/output/ and write a summary of errors and warnings across all processed logs." \
--schedule "daily at 23:00" \
--output daily-digest
4. Start the watcher:
Now whenever a log file is modified in /var/log/myapp/, the log-ingest skill runs automatically. At 11 PM, Claude summarizes the day's findings and sends them to the daily-digest notification profile.
Example 4: Adding a Custom CLI Command with Dashboard Integration¶
Create a claw inventory command that tracks hardware inventory, with a matching dashboard API endpoint.
1. Create the CLI module.
superpowers/cli_inventory.py:
"""Click subcommands for hardware inventory tracking."""
from __future__ import annotations
import json
from pathlib import Path
import click
from rich.console import Console
from rich.table import Table
from superpowers.config import get_data_dir
console = Console()
def _inventory_path() -> Path:
return get_data_dir() / "inventory.json"
def _load() -> list[dict]:
path = _inventory_path()
if not path.exists():
return []
return json.loads(path.read_text())
def _save(items: list[dict]) -> None:
path = _inventory_path()
path.write_text(json.dumps(items, indent=2))
@click.group("inventory")
def inventory_group():
"""Track hardware inventory."""
@inventory_group.command("list")
def inventory_list():
"""List all inventory items."""
items = _load()
if not items:
console.print("[dim]No inventory items.[/dim]")
return
table = Table(title="Hardware Inventory")
table.add_column("Name", style="cyan")
table.add_column("Type")
table.add_column("IP")
table.add_column("Status")
for item in items:
table.add_row(
item.get("name", ""),
item.get("type", ""),
item.get("ip", ""),
item.get("status", "unknown"),
)
console.print(table)
@inventory_group.command("add")
@click.argument("name")
@click.option("--type", "item_type", default="server", help="Device type")
@click.option("--ip", default="", help="IP address")
def inventory_add(name: str, item_type: str, ip: str):
"""Add an inventory item."""
items = _load()
items.append({"name": name, "type": item_type, "ip": ip, "status": "active"})
_save(items)
console.print(f"[green]Added:[/green] {name}")
2. Register in cli.py:
3. Create the dashboard router.
dashboard/routers/inventory.py:
import json
from pathlib import Path
from fastapi import APIRouter
from superpowers.config import get_data_dir
router = APIRouter()
def _inventory_path() -> Path:
return get_data_dir() / "inventory.json"
@router.get("/")
def list_inventory():
path = _inventory_path()
if not path.exists():
return {"items": []}
return {"items": json.loads(path.read_text())}
@router.post("/")
def add_inventory(name: str, item_type: str = "server", ip: str = ""):
path = _inventory_path()
items = json.loads(path.read_text()) if path.exists() else []
items.append({"name": name, "type": item_type, "ip": ip, "status": "active"})
path.write_text(json.dumps(items, indent=2))
return {"status": "created", "name": name}
4. Register in dashboard/app.py:
from dashboard.routers import inventory
api_router.include_router(
inventory.router, prefix="/inventory", tags=["inventory"],
)
5. Verify:
claw inventory add proxmox --type hypervisor --ip 192.168.30.10
claw inventory add truenas --type storage --ip 192.168.13.69
claw inventory list
# API access
curl -u "admin:pass" http://localhost:8200/api/inventory/
Further Reading¶
| Topic | Document |
|---|---|
| Full config reference | CONFIGURATION.md |
| Security model and hardening | SECURITY.md |
| Deployment guide | DEPLOYMENT.md |
| Upgrade procedures | UPGRADE.md |
| Operational runbooks | RUNBOOKS.md |
| Skill system details | skills.md |
| Workflow engine | workflows.md |
| Cron scheduler | cron.md |
| Messaging channels | messaging.md |
| File watchers | watchers.md |
| Dashboard and API | dashboard.md |
| MCP tools for Claude Code | mcp-server.md |