Custom Skills
Skill Types
There are three types of skills, each suited to different levels of complexity.
| Type | Files | Best For |
|---|---|---|
| Pure Instruction | SKILL.md only | Simple workflows using Claude's built-in tools |
| Python Script | SKILL.md + script.py | API calls, data processing, external integrations |
| Subagent | SKILL.md (with context: fork) | Long-running research, exploratory tasks |
File Structure
Skills are directories containing at minimum a SKILL.md file:
~/.claude/skills/
my-skill/
SKILL.md # Required: instructions and metadata
my_skill.py # Optional: Python script for complex logic
Locations:
| Path | Scope |
|---|---|
~/.claude/skills/ | Personal skills, available in all projects |
.claude/skills/ | Project-specific, scoped to one codebase |
SKILL.md Anatomy
Every skill starts with YAML frontmatter followed by markdown instructions:
---
name: my-skill
description: When Claude should use this skill
allowed-tools: Bash, Read, Write
---
# My Skill
Instructions that Claude follows when this skill is invoked.
## Usage
/my-skill <arguments>
## Process
1. First step
2. Second step
Frontmatter Options
| Field | Type | Description |
|---|---|---|
name | string | Command name -- becomes /name |
description | string | Tells Claude when to auto-invoke this skill |
allowed-tools | string | Comma-separated tools: Bash, Read, Write, Edit, WebSearch, WebFetch, Grep, Glob |
disable-model-invocation | boolean | If true, only the user can invoke (Claude will not auto-trigger) |
user-invocable | boolean | If false, hidden from user menu (Claude-only) |
context | string | Set to fork to run in a subagent with its own context |
agent | string | Agent type for subagents: Explore, Plan, or general-purpose |
Creating a Pure Instruction Skill
Pure instruction skills have no Python code. Claude uses its existing tools (Read, Write, Bash) to follow the instructions.
Example: Quick capture to inbox
---
name: capture
description: Quickly capture a note to the inbox folder
allowed-tools: Write
---
# Quick Capture
Save a quick note to the user's inbox.
## Usage
/capture <note content>
## Process
1. Generate filename: `inbox/YYYY-MM-DD-<slugified-title>.md`
2. Create the file at `$VAULT_PATH/<filename>` with frontmatter:
```markdown
---
tags: []
shared: false
---
<note content>
- Confirm creation to the user
Save this as `~/.claude/skills/capture/SKILL.md` and the `/capture` command is immediately available.
## Creating a Python Script Skill
For skills that need to call APIs, process data, or perform complex logic, add a Python script alongside `SKILL.md`.
### Step 1: Create the Directory
```bash
mkdir -p ~/.claude/skills/my-skill
Step 2: Write the Python Script
Use argparse for subcommands and import the shared API client:
#!/usr/bin/env python3
"""My custom skill."""
import argparse
import sys
from pathlib import Path
# Import the shared Cognova API client
sys.path.insert(0, str(Path(__file__).parent.parent / '_lib'))
from api import get, post, put, delete
def cmd_list(args):
"""List items."""
ok, data = get('/my-endpoint')
if not ok:
print(f"Error: {data}", file=sys.stderr)
sys.exit(1)
for item in data:
print(f"- {item['name']}")
def cmd_create(args):
"""Create an item."""
ok, result = post('/my-endpoint', {'name': args.name})
if ok:
print(f"Created: {result['name']}")
else:
print(f"Error: {result}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description='My Skill')
subparsers = parser.add_subparsers(dest='command', required=True)
subparsers.add_parser('list', help='List items')
create_p = subparsers.add_parser('create', help='Create an item')
create_p.add_argument('name', help='Item name')
args = parser.parse_args()
{'list': cmd_list, 'create': cmd_create}[args.command](args)
if __name__ == '__main__':
main()
Step 3: Write SKILL.md
---
name: my-skill
description: Manage my custom items
allowed-tools: Bash, Read
---
# My Skill
## Commands
\`\`\`bash
python3 ~/.claude/skills/my-skill/my_skill.py <command> [options]
\`\`\`
## Available Commands
- `list` -- show all items
- `create <name>` -- create a new item
Step 4: Test
# Test directly
python3 ~/.claude/skills/my-skill/my_skill.py --help
python3 ~/.claude/skills/my-skill/my_skill.py list
# Test via Claude
claude
> /my-skill list
Using the Shared API Client
The _lib/api.py module provides four HTTP helpers that handle authentication automatically:
from api import get, post, put, delete
# All return (success: bool, data_or_error)
ok, tasks = get('/tasks', params={'status': 'todo'})
ok, result = post('/tasks', {'title': 'New task', 'priority': 2})
ok, result = put('/tasks/abc-123', {'status': 'done'})
ok, result = delete('/tasks/abc-123')
Authentication uses the .api-token file generated during Cognova setup. The client checks multiple paths (project directory, Docker mount, working directory) so it works across all deployment types.
Accessing Secrets
For skills that call external APIs, use the secrets store instead of hardcoding credentials:
from api import get_secret
success, api_key = get_secret("DISCORD_WEBHOOK_URL")
if not success:
print(f"Error: {api_key}")
print("Add this secret at Settings > Secrets in the dashboard")
sys.exit(1)
Secrets are managed through the dashboard at Settings > Secrets and encrypted at rest.
Never hardcode API keys, tokens, or credentials in skill scripts. Always use get_secret() for external service credentials and environment variables for Cognova system configuration.
Using the Skill Creator
The fastest way to build a new skill is to ask Claude directly:
/skill-creator
The wizard guides you through:
- Requirements gathering -- what the skill does, who invokes it, whether it needs external APIs
- Existing solution check -- searches for MCPs, plugins, and community tools that already solve the problem
- Type selection -- determines whether you need a pure instruction skill, Python script, or subagent
- File generation -- creates
SKILL.mdand any supporting scripts - Secrets setup -- shows how to configure API keys if needed
The skill creator searches the web for existing MCP servers before building from scratch. If a maintained MCP already does what you need, it will suggest installing that instead.
Environment Variables
These are available to all skills automatically:
| Variable | Default | Description |
|---|---|---|
COGNOVA_API_URL | http://localhost:3000 | Cognova API base URL |
COGNOVA_API_TOKEN | (auto-generated) | Auth token, also read from .api-token file |
COGNOVA_PROJECT_DIR | -- | Install directory path |
VAULT_PATH | -- | Path to the vault directory |
Real-World Example: Weekly Digest Skill
A Python script skill that summarizes completed tasks and sends a digest to a webhook:
#!/usr/bin/env python3
"""Weekly digest -- summarize completed tasks."""
import json
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent / '_lib'))
from api import get, get_secret
def main():
# Get completed tasks from the last 7 days
ok, tasks = get('/tasks', {'status': 'done'})
if not ok:
print(f"Error: {tasks}", file=sys.stderr)
sys.exit(1)
if not tasks:
print("No completed tasks this week.")
return
# Build summary
summary = f"Completed {len(tasks)} tasks this week:\n"
for task in tasks[:20]:
summary += f"- {task['title']}\n"
# Send to webhook
ok, webhook_url = get_secret("DIGEST_WEBHOOK_URL")
if ok:
subprocess.run([
"curl", "-sL", "-X", "POST",
"-H", "Content-Type: application/json",
"-d", json.dumps({"text": summary}),
webhook_url
], capture_output=True)
print(f"Digest sent with {len(tasks)} tasks.")
else:
# Fall back to printing
print(summary)
if __name__ == '__main__':
main()
This pattern -- fetch data from Cognova, transform it, send it somewhere -- is the foundation of most custom skills.