From 73400a21abc422723435acb9e79a9ead5df6fae0 Mon Sep 17 00:00:00 2001 From: OpenCode Test Date: Sun, 4 Jan 2026 12:34:39 -0800 Subject: [PATCH] Add UserPromptSubmit hook for context injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- hooks/hooks.json | 11 ++ hooks/scripts/prompt-context.py | 173 ++++++++++++++++++ .../general-instructions.json | 12 ++ 3 files changed, 196 insertions(+) create mode 100755 hooks/scripts/prompt-context.py diff --git a/hooks/hooks.json b/hooks/hooks.json index 428cbd3..0d79878 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,5 +1,16 @@ { "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "~/.claude/hooks/scripts/prompt-context.py", + "timeout": 5 + } + ] + } + ], "SessionStart": [ { "hooks": [ diff --git a/hooks/scripts/prompt-context.py b/hooks/scripts/prompt-context.py new file mode 100755 index 0000000..e0072fb --- /dev/null +++ b/hooks/scripts/prompt-context.py @@ -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() diff --git a/state/personal-assistant/general-instructions.json b/state/personal-assistant/general-instructions.json index abef28a..c66585c 100644 --- a/state/personal-assistant/general-instructions.json +++ b/state/personal-assistant/general-instructions.json @@ -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" } ] }