diff --git a/docs/docs.json b/docs/docs.json
index f153213..5d563b4 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -227,7 +227,8 @@
"group": "Building",
"pages": [
"guides/building/droid-exec-tutorial",
- "guides/building/droid-vps-setup"
+ "guides/building/droid-vps-setup",
+ "guides/building/context7-hooks"
]
}
]
diff --git a/docs/guides/building/context7-hooks.mdx b/docs/guides/building/context7-hooks.mdx
new file mode 100644
index 0000000..24f851c
--- /dev/null
+++ b/docs/guides/building/context7-hooks.mdx
@@ -0,0 +1,473 @@
+---
+title: "Context7 Hooks: Token Caps + Auto Archive"
+description: "Cap the number of tokens retrieved by Context7"
+---
+
+
+ Context7 already ships in Factory’s MCP registry. Once you authenticate it, you can plug in custom hooks to keep documentation pulls lightweight, logged, and repeatable.
+
+
+
+ New to hooks? Start with [Get started with hooks](/cli/configuration/hooks-guide) for a walkthrough of the hooks UI and configuration model, then come back here to plug in the Context7-specific scripts.
+
+
+## Prerequisites
+
+- Factory CLI installed
+- Context7 account + API token for MCP auth
+- `jq` installed (`brew install jq` on macOS)
+- A text editor—everything below builds the scripts from scratch
+- Hooks feature enabled (run `/settings`, toggle **Hooks** to **Enabled** so the `/hooks` command is available)
+
+## Step 1 · Authenticate the Context7 MCP connector
+
+
+
+ Run the `/mcp` slash command to open the MCP manager. From the **Registry** list, select the `context7` entry to add it, then in the server detail view choose **Authenticate**. Follow the browser prompt; credentials are saved to `~/.factory/mcp-oauth.json`.
+
+
+ Open `/mcp` again, select the `context7` server, and confirm it shows as enabled and authenticated (you should be able to view its tools, including `get-library-docs`). If not, run **Authenticate** again.
+
+
+
+## Step 2 · Create the hook scripts
+
+You can either have droid generate the scripts for you, or use a copy‑paste template.
+
+### Option A · Ask droid to generate the scripts
+
+If you want hooks in a project, in your project root, start a droid session and give it a prompt like:
+
+```text
+In this repo, create ~/.factory/hooks/context7_token_limiter.sh and ~/.factory/hooks/context7_archive.sh.
+The first should be a PreToolUse hook that enforces a MAX_TOKENS limit (3000) on the tool context7___get-library-docs.
+The second should archive every successful response as Markdown with YAML frontmatter into ${FACTORY_PROJECT_DIR:-$PWD}/context7-archive.
+Use jq and follow the hooks JSON input/output contracts from the hooks reference docs.
+```
+
+Review droid’s proposal, tweak as needed, then save the scripts under `~/.factory/hooks/` and make them executable:
+
+```bash
+chmod +x ~/.factory/hooks/context7_token_limiter.sh ~/.factory/hooks/context7_archive.sh
+```
+
+### Option B · Use the reference template
+
+Ensure the `~/.factory/hooks` directory exists, then create these two files.
+
+**`~/.factory/hooks/context7_token_limiter.sh`**
+
+```bash
+#!/usr/bin/env bash
+#
+# Context7 MCP Token Limiter Hook v2
+# Blocks context7___get-library-docs calls with tokens > MAX_TOKENS
+#
+# Exit Codes:
+# 0 - Allow (tokens <= MAX_TOKENS or not specified)
+# 1 - Allow with warning (invalid tokens parameter)
+# 2 - Block and provide feedback to Claude
+#
+# Notes:
+# - Environment variables MAX_TOKENS and LOG_FILE can override defaults
+# - Robust jq parsing handles strings, floats, missing values
+# - Logging never fails the hook
+# - Gracefully allows if jq is missing
+
+set -euo pipefail
+umask 077
+
+# Configuration with env overrides
+MAX_TOKENS="${MAX_TOKENS:-3000}"
+LOG_FILE="${LOG_FILE:-$HOME/.factory/hooks.log}"
+
+ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
+
+log() {
+ # Best effort logging; never fail the hook because of logging
+ local msg="$1"
+ mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true
+ printf "[%s] %s\n" "$(ts)" "$msg" >> "$LOG_FILE" 2>/dev/null || true
+}
+
+# Ensure jq exists; if not, allow rather than blocking tooling
+if ! command -v jq >/dev/null 2>&1; then
+ printf "Warning: jq not found. Allowing tool call.\n" >&2
+ exit 0
+fi
+
+# Read JSON from stdin
+input="$(cat)"
+
+# Parse tool name (empty on parse errors)
+tool_name="$(printf "%s" "$input" | jq -r '.tool_name // empty' 2>/dev/null || printf "")"
+
+# Only validate Context7 get-library-docs tool
+if [[ "$tool_name" != "context7___get-library-docs" ]]; then
+ exit 0
+fi
+
+# Extract tokens as an integer using jq; empty if missing or null
+# floor ensures integer comparison even if caller passes a float
+tokens="$(printf "%s" "$input" \
+ | jq -r 'if (.tool_input.tokens? // null) == null then "" else (.tool_input.tokens | tonumber | floor) end' \
+ 2>/dev/null || printf "")"
+
+log "Context7 validation: tool=$tool_name tokens=${tokens:-} limit=$MAX_TOKENS"
+
+# If tokens missing or empty, allow and let Context7 defaults apply
+if [[ -z "${tokens}" ]]; then
+ exit 0
+fi
+
+# Validate tokens is an integer string
+if ! [[ "$tokens" =~ ^[0-9]+$ ]]; then
+ printf "Warning: invalid tokens parameter: %s\n" "$tokens" >&2
+ # Non-fatal warning; preserve original intent
+ exit 1
+fi
+
+# Enforce limit
+if (( tokens > MAX_TOKENS )); then
+ log "BLOCKED: Context7 call with $tokens tokens (limit: $MAX_TOKENS)"
+
+ # Feedback to Claude. Exit code 2 signals a block in PreToolUse hooks.
+ cat >&2 < $MAX_TOKENS
+
+We prefer an iterative approach to Context7 queries for better context management:
+
+1. Start with your first query (max $MAX_TOKENS tokens) on the most important topic
+2. Review the results and identify what additional information you need
+3. Refine your next query based on what you learned
+4. Repeat with focused follow-up queries
+
+This iterative pattern gives you:
+- Better control over context window usage
+- More focused, relevant results per query
+- Ability to adapt your research based on findings
+- Less risk of overwhelming the context with broad searches
+
+Example workflow:
+ Query 1: tokens=3000, topic="React 19 new hooks and features"
+ (review results, identify gaps)
+ Query 2: tokens=3000, topic="use() hook detailed patterns and examples"
+ (review results, go deeper)
+ Query 3: tokens=3000, topic="Server Actions with React 19 integration"
+
+Start with your most important question at 3000 tokens, then iterate.
+EOF
+
+ exit 2
+fi
+
+# Allow - tokens within acceptable range
+exit 0
+```
+
+**`~/.factory/hooks/context7_archive.sh`**
+
+```bash
+#!/bin/bash
+#
+# Context7 MCP Archive Hook v2
+# Saves Context7 query results to disk for future reference
+# PostToolUse hook for context7___get-library-docs
+#
+# Filename format: {YYYYMMDD}_{project-name}_{library-slug}_{topic-slug}.md
+# Uses underscore for field separators, hyphen for word separators
+#
+# Environment variables:
+# DEBUG=1 - Enable debug logging
+# RAW_JSON=1 - Save raw JSON tool_response instead of parsed text
+#
+
+set -euo pipefail
+
+# Security: restrictive file permissions
+umask 077
+
+# Configuration
+ARCHIVE_DIR="${FACTORY_PROJECT_DIR}/context7-archive"
+MAX_TOPIC_LENGTH=50
+DEBUG_LOG="${ARCHIVE_DIR}/hook-debug.log"
+
+# Debug function
+debug() {
+ if [[ "${DEBUG:-0}" == "1" ]]; then
+ echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] $*" >> "$DEBUG_LOG"
+ fi
+}
+
+# Check for jq dependency
+if ! command -v jq &> /dev/null; then
+ echo "Warning: jq not found. Context7 archive hook disabled." >&2
+ exit 0
+fi
+
+# Read JSON from stdin
+input=$(cat)
+
+# Create archive directory
+mkdir -p "$ARCHIVE_DIR"
+
+debug "Hook invoked"
+
+# Only process Context7 get-library-docs tool
+tool_name=$(echo "$input" | jq -r '.tool_name // empty' 2>/dev/null || echo "")
+debug "Tool: $tool_name"
+if [[ "$tool_name" != "context7___get-library-docs" ]]; then
+ debug "Skipping non-Context7 tool"
+ exit 0
+fi
+
+# Extract data from hook input
+library_id=$(echo "$input" | jq -r '.tool_input.context7CompatibleLibraryID // "unknown-library"' 2>/dev/null || echo "unknown-library")
+topic=$(echo "$input" | jq -r '.tool_input.topic // "untitled"' 2>/dev/null || echo "untitled")
+tokens=$(echo "$input" | jq -r '.tool_input.tokens // "unknown"' 2>/dev/null || echo "unknown")
+
+# Debug: log the tool_response type
+response_type=$(echo "$input" | jq -r '.tool_response | type' 2>/dev/null || echo "unknown")
+debug "tool_response type: $response_type"
+
+# Extract results - handle string, array, or object with text field
+# This is the robust parsing logic
+if [[ "$response_type" == "string" ]]; then
+ results=$(echo "$input" | jq -r '.tool_response' 2>/dev/null || echo "")
+elif [[ "$response_type" == "array" ]]; then
+ # Array of content parts - join them
+ results=$(echo "$input" | jq -r '.tool_response | if type == "array" then map(if type == "object" and has("text") then .text else . end) | join("\n\n") else . end' 2>/dev/null || echo "")
+elif [[ "$response_type" == "object" ]]; then
+ # Object with text field
+ results=$(echo "$input" | jq -r '.tool_response.text // .tool_response | if type == "string" then . else tojson end' 2>/dev/null || echo "")
+else
+ # Fallback: stringify whatever it is
+ results=$(echo "$input" | jq -r '.tool_response | if type == "string" then . else tojson end' 2>/dev/null || echo "")
+fi
+
+# RAW_JSON mode: save the full JSON tool_response
+if [[ "${RAW_JSON:-0}" == "1" ]]; then
+ results=$(echo "$input" | jq '.tool_response' 2>/dev/null || echo '{}')
+fi
+
+# Skip if no results
+if [[ -z "$results" || "$results" == "null" ]]; then
+ debug "No results to archive"
+ exit 0
+fi
+
+debug "Archiving: lib=$library_id topic=$topic tokens=$tokens"
+
+# Extract project name from FACTORY_PROJECT_DIR
+project_name=$(basename "$FACTORY_PROJECT_DIR" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
+
+# Extract library slug from library ID with improved logic
+# /vercel/next.js/v15.1.8 -> nextjs
+# /ericbuess/claude-code-docs -> claude-code
+# /cloudflare/workers-sdk -> cloudflare-workers
+#
+# Strategy:
+# 1. Remove leading slash
+# 2. Split on slash: org / project / version
+# 3. Use project part (index 1)
+# 4. Remove common prefixes (workers-, etc.)
+# 5. Clean up: lowercase, replace non-alnum with hyphen, collapse multiple hyphens
+
+library_path="${library_id#/}" # Remove leading slash
+IFS='/' read -ra parts <<< "$library_path"
+
+if [[ ${#parts[@]} -ge 2 ]]; then
+ org="${parts[0]}"
+ project="${parts[1]}"
+
+ # Derive slug from project name
+ library_slug=$(echo "$project" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')
+
+ # Special handling for known patterns
+ # next.js -> nextjs
+ library_slug=$(echo "$library_slug" | sed 's/next-js/nextjs/')
+ # workers-sdk -> cloudflare-workers (prepend org for clarity)
+ if [[ "$project" =~ workers && "$org" == "cloudflare" ]]; then
+ library_slug="cloudflare-workers"
+ fi
+ # Remove common prefixes that add no value
+ library_slug=$(echo "$library_slug" | sed 's/^docs-//' | sed 's/-docs$//')
+else
+ # Fallback: use entire library_id as slug
+ library_slug=$(echo "$library_id" | tr '[:upper:]' '[:lower:]' | sed 's|/|-|g' | sed 's/[^a-z0-9-]/-/g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//')
+fi
+
+# Ensure we have a valid slug
+if [[ -z "$library_slug" ]]; then
+ library_slug="unknown"
+fi
+
+debug "Library slug: $library_slug"
+
+# Create topic slug (kebab-case, max 50 chars at word boundary)
+topic_slug=$(echo "$topic" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ */-/g' | sed 's/^-//' | sed 's/-$//')
+
+# Handle empty topic
+if [[ -z "$topic_slug" ]]; then
+ topic_slug="untitled"
+fi
+
+# Truncate at word boundary if too long
+if [[ ${#topic_slug} -gt $MAX_TOPIC_LENGTH ]]; then
+ topic_slug="${topic_slug:0:$MAX_TOPIC_LENGTH}"
+ # Trim to last complete word (hyphen-separated)
+ if [[ "$topic_slug" =~ -.*$ ]]; then
+ topic_slug="${topic_slug%-*}"
+ fi
+fi
+
+debug "Topic slug: $topic_slug"
+
+# Generate timestamp and filename
+date_only=$(date +"%Y%m%d")
+base_filename="${date_only}_${project_name}_${library_slug}_${topic_slug}"
+filename="${base_filename}.md"
+filepath="${ARCHIVE_DIR}/${filename}"
+
+# Collision handling: add -N suffix if file exists
+counter=1
+while [[ -f "$filepath" ]]; do
+ filename="${base_filename}-${counter}.md"
+ filepath="${ARCHIVE_DIR}/${filename}"
+ ((counter++))
+ if [[ $counter -gt 100 ]]; then
+ echo "Error: Too many collisions for filename: $base_filename" >&2
+ exit 1
+ fi
+done
+
+debug "Final filename: $filename"
+
+# Create markdown file with YAML frontmatter
+cat > "$filepath" <
+ The matcher should target the LLM tool name `context7___get-library-docs`. If you’re unsure, inspect `~/.factory/hooks.log` (written by the limiter) or open the latest transcript file (see the `transcript_path` field, typically under `~/.factory/projects/...`) to inspect the `tool_name` value.
+
+
+## Step 3 · Token limiter (PreToolUse)
+
+- **Hook file:** `~/.factory/hooks/context7_token_limiter.sh`
+- **Purpose:** Block any `context7___get-library-docs` call that requests more than 3,000 tokens.
+- **Useful env vars:**
+
+```bash
+export MAX_TOKENS=3000
+export LOG_FILE="$HOME/.factory/hooks.log" # Optional auditing
+```
+
+When the script exits with code 2, Factory halts the tool call and surfaces the warning text to the assistant.
+
+## Step 4 · Archive writer (PostToolUse)
+
+- **Hook file:** `~/.factory/hooks/context7_archive.sh`
+- **Purpose:** Save every successful Context7 response as Markdown in `${FACTORY_PROJECT_DIR}/context7-archive` (falls back to your current repo if the env var is unset).
+- **Useful env vars:**
+
+```bash
+export DEBUG=1 # Verbose logging to hook-debug.log
+export ARCHIVE_DIR="$HOME/context7-history" # Optional custom location
+export RAW_JSON=1 # Store raw JSON payloads instead of rendered text
+```
+
+Each file includes YAML frontmatter so you can grep or index entries later (e.g., `20251114_myapp_nextjs_server-actions.md`).
+
+## Step 5 · Register the hooks
+
+You can register these hooks either through the `/hooks` UI (recommended) or by editing `~/.factory/settings.json` directly.
+
+### Option A - Use the Hooks UI
+
+1. Run `/settings` and make sure **Hooks** is set to **Enabled**.
+2. Run `/hooks`, select the **PreToolUse** event, and add a matcher `context7___get-library-docs`. Hit enter to save.
+3. Add a `command`: `~/.factory/hooks/context7_token_limiter.sh`, and store it in **User settings**.
+4. Repeat for **PostToolUse**, matcher `context7___get-library-docs`, command `~/.factory/hooks/context7_archive.sh`.
+
+### Option B - Edit settings JSON
+
+Open `~/.factory/settings.json` and add a `hooks` block like this (merging with any existing hooks):
+
+```jsonc
+{
+ "hooks": {
+ "PreToolUse": [
+ {
+ "matcher": "context7___get-library-docs",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "~/.factory/hooks/context7_token_limiter.sh",
+ "timeout": 5000
+ }
+ ]
+ }
+ ],
+ "PostToolUse": [
+ {
+ "matcher": "context7___get-library-docs",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "~/.factory/hooks/context7_archive.sh",
+ "timeout": 5000
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+Using the exact LLM tool name `context7___get-library-docs` ensures you only target the Context7 docs fetch tool. You can also use regex matchers (see [Hooks reference](/reference/hooks-reference)) if you need to match multiple Context7 tools.
+
+Restart Factory (or reopen your session) after editing your hooks configuration.
+
+## Step 6 · Test the workflow
+
+
+
+ Ask Context7 for something intentionally huge, e.g. “Pull the entire Factory documentation with context7 mcp". The hook should block it at 3,000 tokens. (Factory already has a local codemap for its docs; this request is purely for testing.)
+
+
+ Run a normal Context7 request. Confirm `context7-archive/` now contains a timestamped Markdown file with the query results.
+
+
+
+## Troubleshooting & customization
+
+- **Matcher typos:** If the hooks never run, double-check the matcher value against `context7___get-library-docs`. One missing underscore is enough to break it.
+- **Missing `jq`:** Install it with `brew install jq` (macOS) or your distro’s package manager.
+- **Permissions:** Ensure every script in `~/.factory/hooks` is executable (`chmod +x ~/.factory/hooks/*`).
+- **Archive clutter:** Add `context7-archive/` to `.gitignore` if you don’t plan to commit the saved docs.
+- **Timeouts:** Increase the `timeout` field in `hooks.json` if you routinely archive very large responses.
+
+With these two hooks in place, every Context7 pull stays within a predictable token budget and automatically lands in a searchable knowledge base.