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:
OpenCode Test
2026-01-04 12:34:39 -08:00
parent 56b455a074
commit 73400a21ab
3 changed files with 196 additions and 0 deletions

View File

@@ -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
View 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()

View File

@@ -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"
}
]
}