Add UserPromptSubmit hook for context injection
Injects contextual information when user submits a prompt: - Current time with period (morning/afternoon/evening/night) - Git branch if in a repository - Relevant memory items based on prompt keywords (2+ matches) - Pending decisions needing attention Design: - Skips short prompts (<10 chars) to not slow down commands - 5s timeout to keep prompts responsive - Lightweight keyword matching for memory relevance Also updates general-instructions.json with git workflow notes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,16 @@
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "~/.claude/hooks/scripts/prompt-context.py",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
|
||||
173
hooks/scripts/prompt-context.py
Executable file
173
hooks/scripts/prompt-context.py
Executable file
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
UserPromptSubmit hook - inject contextual information based on prompt.
|
||||
|
||||
Injects:
|
||||
- Time-aware context
|
||||
- Current git branch (if in a repo)
|
||||
- Relevant memory items based on prompt keywords
|
||||
- Pending decisions needing attention
|
||||
|
||||
Output goes to stdout and is added to Claude's context.
|
||||
Keep this fast (<5s) to not slow down prompts.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Paths
|
||||
STATE_DIR = Path.home() / ".claude/state/personal-assistant"
|
||||
MEMORY_DIR = STATE_DIR / "memory"
|
||||
|
||||
|
||||
def get_time_context() -> str:
|
||||
"""Get time-aware greeting context."""
|
||||
hour = datetime.now().hour
|
||||
if 5 <= hour < 12:
|
||||
period = "morning"
|
||||
elif 12 <= hour < 17:
|
||||
period = "afternoon"
|
||||
elif 17 <= hour < 21:
|
||||
period = "evening"
|
||||
else:
|
||||
period = "night"
|
||||
|
||||
return f"Current time: {datetime.now().strftime('%Y-%m-%d %H:%M')} ({period})"
|
||||
|
||||
|
||||
def get_git_context(cwd: str) -> str | None:
|
||||
"""Get current git branch if in a repo."""
|
||||
if not cwd:
|
||||
return None
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--abbrev-ref", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2,
|
||||
cwd=cwd
|
||||
)
|
||||
if result.returncode == 0:
|
||||
branch = result.stdout.strip()
|
||||
if branch:
|
||||
return f"Git branch: {branch}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_relevant_memory(prompt: str, limit: int = 3) -> list[str]:
|
||||
"""Find memory items relevant to the prompt."""
|
||||
relevant = []
|
||||
prompt_lower = prompt.lower()
|
||||
|
||||
# Keywords to look for
|
||||
keywords = set(re.findall(r'\b\w{4,}\b', prompt_lower))
|
||||
|
||||
if not keywords:
|
||||
return relevant
|
||||
|
||||
# Check each memory file
|
||||
for memory_file in ["decisions.json", "preferences.json", "projects.json"]:
|
||||
path = MEMORY_DIR / memory_file
|
||||
if not path.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
with open(path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
for item in data.get("items", []):
|
||||
content = item.get("content", "").lower()
|
||||
context = item.get("context", "").lower()
|
||||
|
||||
# Check for keyword matches
|
||||
item_words = set(re.findall(r'\b\w{4,}\b', content + " " + context))
|
||||
matches = keywords & item_words
|
||||
|
||||
if len(matches) >= 2: # Require at least 2 matching keywords
|
||||
category = memory_file.replace(".json", "").rstrip("s")
|
||||
relevant.append(f"[{category}] {item.get('content', '')}")
|
||||
|
||||
if len(relevant) >= limit:
|
||||
return relevant
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return relevant
|
||||
|
||||
|
||||
def get_pending_decisions(limit: int = 2) -> list[str]:
|
||||
"""Get recent pending decisions."""
|
||||
pending = []
|
||||
decisions_path = MEMORY_DIR / "decisions.json"
|
||||
|
||||
if not decisions_path.exists():
|
||||
return pending
|
||||
|
||||
try:
|
||||
with open(decisions_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Get most recent decisions (they might need follow-up)
|
||||
items = data.get("items", [])
|
||||
for item in items[-limit:]:
|
||||
if item.get("status") == "pending":
|
||||
pending.append(f"Pending: {item.get('content', '')}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return pending
|
||||
|
||||
|
||||
def main():
|
||||
# Read hook input from stdin
|
||||
try:
|
||||
input_data = json.load(sys.stdin)
|
||||
except json.JSONDecodeError:
|
||||
return
|
||||
|
||||
prompt = input_data.get("prompt", "")
|
||||
cwd = input_data.get("cwd", "")
|
||||
|
||||
# Skip context injection for very short prompts (likely commands)
|
||||
if len(prompt) < 10:
|
||||
return
|
||||
|
||||
# Gather context
|
||||
context_parts = []
|
||||
|
||||
# Time context (always include)
|
||||
context_parts.append(get_time_context())
|
||||
|
||||
# Git context (if in a repo)
|
||||
git_ctx = get_git_context(cwd)
|
||||
if git_ctx:
|
||||
context_parts.append(git_ctx)
|
||||
|
||||
# Relevant memory (if prompt has substance)
|
||||
if len(prompt) > 20:
|
||||
relevant = get_relevant_memory(prompt)
|
||||
if relevant:
|
||||
context_parts.append("Relevant memory:")
|
||||
context_parts.extend(f" - {item}" for item in relevant)
|
||||
|
||||
# Pending decisions (occasionally remind)
|
||||
pending = get_pending_decisions()
|
||||
if pending:
|
||||
context_parts.extend(pending)
|
||||
|
||||
# Output context (will be injected into Claude's context)
|
||||
if context_parts:
|
||||
print("\n".join(context_parts))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -7,6 +7,18 @@
|
||||
"instruction": "Delegate technical tasks to specialized agents. Use skills and slash commands for quick technical checks.",
|
||||
"status": "active",
|
||||
"added": "2025-01-21"
|
||||
},
|
||||
{
|
||||
"id": "b2c3d4e5-6789-01bc-def0-222222222222",
|
||||
"instruction": "Create a new git branch when working on new features (e.g., feature/descriptive-name). Commit work to the branch before rebasing and pushing.",
|
||||
"status": "active",
|
||||
"added": "2026-01-03"
|
||||
},
|
||||
{
|
||||
"id": "c3d4e5f6-7890-12cd-ef01-333333333333",
|
||||
"instruction": "The ~/.claude repo uses Gitea (not GitHub). After pushing a branch, create PR manually via Gitea web UI - gh CLI won't work.",
|
||||
"status": "active",
|
||||
"added": "2026-01-03"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user