Hooks
Hooks are Python scripts that automatically run in response to CommandLane events. Use them to automate workflows, add custom tagging, send notifications, or integrate with external services.
Quick Start
-
Create the hooks directory:
mkdir ~/.cmdlane/hooks -
Create a hook file (e.g.,
on_capture.py):#!/usr/bin/env python3
import sys
import json
# Read event data from stdin
event = json.loads(sys.stdin.read())
# Modify the entry
entry = event.get("entry", {})
entry["tags"] = entry.get("tags", []) + ["auto-tagged"]
event["entry"] = entry
# Output modified data
print(json.dumps(event)) -
The hook will run automatically on every capture.
Available Hook Events
| Filename | Event | When It Runs |
|---|---|---|
on_capture.py | capture.created | After classification, before storage |
on_task.py | task.status_changed | When a task status changes |
on_task_due.py | task.due_changed | When a task due date changes |
on_delete.py | entry.deleted | When an entry is deleted |
on_startup.py | app.startup | When CommandLane starts |
Hook Protocol
Hooks communicate via JSON through stdin/stdout:
Input (stdin)
{
"event": "capture.created",
"entry": {
"body": "Meeting notes from standup",
"entry_type": "note",
"tags": [],
"source_app": "VSCode",
"source_title": "project.py"
}
}
Output (stdout)
Return modified JSON to change the entry:
{
"event": "capture.created",
"entry": {
"body": "Meeting notes from standup",
"entry_type": "note",
"tags": ["meeting", "standup"],
"source_app": "VSCode",
"source_title": "project.py"
}
}
If your hook doesn't need to modify data, you can output nothing or the unchanged event.
Example Hooks
Auto-Tag by Source App
Tag entries based on which application you captured from:
#!/usr/bin/env python3
import sys
import json
event = json.loads(sys.stdin.read())
entry = event.get("entry", {})
tags = list(entry.get("tags", []))
source_app = entry.get("source_app", "").lower()
# Add tags based on source application
if "code" in source_app or "cursor" in source_app:
tags.append("dev")
elif "chrome" in source_app or "firefox" in source_app:
tags.append("web")
elif "slack" in source_app or "teams" in source_app:
tags.append("communication")
elif "outlook" in source_app:
tags.append("email")
entry["tags"] = list(set(tags)) # Remove duplicates
event["entry"] = entry
print(json.dumps(event))
Send Webhook Notification
Notify an external service when tasks are created:
#!/usr/bin/env python3
import sys
import json
import urllib.request
event = json.loads(sys.stdin.read())
entry = event.get("entry", {})
# Only notify for tasks
if entry.get("entry_type") == "task":
webhook_url = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
payload = {
"text": f"New task: {entry.get('body', '')[:100]}"
}
req = urllib.request.Request(
webhook_url,
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"}
)
try:
urllib.request.urlopen(req, timeout=3)
except Exception:
pass # Don't break capture on webhook failure
# Pass through unchanged
print(json.dumps(event))
Auto-Set Project from Window Title
Extract project names from VS Code window titles:
#!/usr/bin/env python3
import sys
import json
import re
event = json.loads(sys.stdin.read())
entry = event.get("entry", {})
title = entry.get("source_title", "")
# Extract project from VS Code title (format: "file.py - ProjectName - Visual Studio Code")
match = re.search(r" - ([^-]+) - (Visual Studio Code|Cursor)", title)
if match:
project = match.group(1).strip().lower().replace(" ", "-")
tags = list(entry.get("tags", []))
tags.append(f"project:{project}")
entry["tags"] = list(set(tags))
event["entry"] = entry
print(json.dumps(event))
Log All Captures
Simple logging hook for debugging:
#!/usr/bin/env python3
import sys
import json
from datetime import datetime
from pathlib import Path
event = json.loads(sys.stdin.read())
entry = event.get("entry", {})
log_file = Path.home() / ".cmdlane" / "capture.log"
with open(log_file, "a") as f:
f.write(f"{datetime.now().isoformat()} | {entry.get('entry_type')} | {entry.get('body', '')[:50]}\n")
# Pass through unchanged
print(json.dumps(event))
Best Practices
Error Handling
Hooks have a 5-second timeout. Always handle errors gracefully:
#!/usr/bin/env python3
import sys
import json
try:
event = json.loads(sys.stdin.read())
# Your logic here
print(json.dumps(event))
except Exception as e:
# Log error but don't break capture
sys.stderr.write(f"Hook error: {e}\n")
# Pass through original input if possible
sys.exit(0)
Performance
- Keep hooks fast (under 1 second ideally)
- Avoid blocking network calls when possible
- Use timeouts for external requests
- Don't read large files synchronously
Chaining
Multiple hooks for the same event run sequentially. Each hook receives the output of the previous one:
on_capture.py (1) → on_capture.py (2) → on_capture.py (3) → Storage
Name hooks with prefixes to control order: 01_validate.py, 02_tag.py, 03_notify.py
Debugging Hooks
Check Discovered Hooks
from pkb.hooks import get_hook_executor
executor = get_hook_executor()
print(executor.list_hooks())
# {'capture.created': ['on_capture.py'], 'app.startup': ['on_startup.py']}
Test Hook Manually
echo '{"event": "capture.created", "entry": {"body": "test", "tags": []}}' | python ~/.cmdlane/hooks/on_capture.py
View Hook Errors
Hook errors are logged but don't interrupt capture. Check the application logs in ~/.cmdlane/logs/.
Refreshing Hooks
Hooks are discovered at startup. If you add or remove hooks while CommandLane is running:
- Restart CommandLane, or
- The hooks will be picked up on next startup
Security Notes
- Hooks run with your user permissions
- Don't store secrets in hook files
- Be cautious with hooks that make network requests
- Hooks from untrusted sources could be malicious