Skip to main content

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

  1. Create the hooks directory:

    mkdir ~/.cmdlane/hooks
  2. 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))
  3. The hook will run automatically on every capture.

Available Hook Events

FilenameEventWhen It Runs
on_capture.pycapture.createdAfter classification, before storage
on_task.pytask.status_changedWhen a task status changes
on_task_due.pytask.due_changedWhen a task due date changes
on_delete.pyentry.deletedWhen an entry is deleted
on_startup.pyapp.startupWhen 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:

  1. Restart CommandLane, or
  2. 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