|
| 1 | +--- |
| 2 | +title: "Context7 Hooks: Token Caps + Auto Archive" |
| 3 | +description: "Cap the number of tokens retrieved by Context7" |
| 4 | +--- |
| 5 | + |
| 6 | +<Note> |
| 7 | + 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. |
| 8 | +</Note> |
| 9 | + |
| 10 | +<Tip> |
| 11 | + 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. |
| 12 | +</Tip> |
| 13 | + |
| 14 | +## Prerequisites |
| 15 | + |
| 16 | +- Factory CLI installed |
| 17 | +- Context7 account + API token for MCP auth |
| 18 | +- `jq` installed (`brew install jq` on macOS) |
| 19 | +- A text editor—everything below builds the scripts from scratch |
| 20 | +- Hooks feature enabled (run `/settings`, toggle **Hooks** to **Enabled** so the `/hooks` command is available) |
| 21 | + |
| 22 | +## Step 1 · Authenticate the Context7 MCP connector |
| 23 | + |
| 24 | +<Steps> |
| 25 | + <Step title="Start the auth flow"> |
| 26 | + 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`. |
| 27 | + </Step> |
| 28 | + <Step title="Verify connectivity"> |
| 29 | + 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. |
| 30 | + </Step> |
| 31 | +</Steps> |
| 32 | + |
| 33 | +## Step 2 · Create the hook scripts |
| 34 | + |
| 35 | +You can either have droid generate the scripts for you, or use a copy‑paste template. |
| 36 | + |
| 37 | +### Option A · Ask droid to generate the scripts |
| 38 | + |
| 39 | +If you want hooks in a project, in your project root, start a droid session and give it a prompt like: |
| 40 | + |
| 41 | +```text |
| 42 | +In this repo, create ~/.factory/hooks/context7_token_limiter.sh and ~/.factory/hooks/context7_archive.sh. |
| 43 | +The first should be a PreToolUse hook that enforces a MAX_TOKENS limit (3000) on the tool context7___get-library-docs. |
| 44 | +The second should archive every successful response as Markdown with YAML frontmatter into ${FACTORY_PROJECT_DIR:-$PWD}/context7-archive. |
| 45 | +Use jq and follow the hooks JSON input/output contracts from the hooks reference docs. |
| 46 | +``` |
| 47 | + |
| 48 | +Review droid’s proposal, tweak as needed, then save the scripts under `~/.factory/hooks/` and make them executable: |
| 49 | + |
| 50 | +```bash |
| 51 | +chmod +x ~/.factory/hooks/context7_token_limiter.sh ~/.factory/hooks/context7_archive.sh |
| 52 | +``` |
| 53 | + |
| 54 | +### Option B · Use the reference template |
| 55 | + |
| 56 | +Ensure the `~/.factory/hooks` directory exists, then create these two files. |
| 57 | + |
| 58 | +**`~/.factory/hooks/context7_token_limiter.sh`** |
| 59 | + |
| 60 | +```bash |
| 61 | +#!/usr/bin/env bash |
| 62 | +set -euo pipefail |
| 63 | +umask 077 |
| 64 | + |
| 65 | +MAX_TOKENS="${MAX_TOKENS:-3000}" |
| 66 | +LOG_FILE="${LOG_FILE:-$HOME/.factory/hooks/context7.log}" |
| 67 | + |
| 68 | +ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; } |
| 69 | +log() { |
| 70 | + mkdir -p "$(dirname "$LOG_FILE")" 2>/dev/null || true |
| 71 | + printf "[%s] %s\n" "$(ts)" "$1" >> "$LOG_FILE" 2>/dev/null || true |
| 72 | +} |
| 73 | + |
| 74 | +if ! command -v jq >/dev/null 2>&1; then |
| 75 | + printf "Warning: jq not found. Allowing tool call.\n" >&2 |
| 76 | + exit 0 |
| 77 | +fi |
| 78 | + |
| 79 | +payload="$(cat)" |
| 80 | +tool_name="$(printf "%s" "$payload" | jq -r '.tool_name // empty' 2>/dev/null || printf "")" |
| 81 | + |
| 82 | +if [[ "$tool_name" != "context7___get-library-docs" ]]; then |
| 83 | + exit 0 |
| 84 | +fi |
| 85 | + |
| 86 | +tokens="$(printf "%s" "$payload" | jq -r 'if (.tool_input.tokens? // null) == null then "" else (.tool_input.tokens | tonumber | floor) end' 2>/dev/null || printf "")" |
| 87 | + |
| 88 | +if [[ -z "$tokens" ]]; then |
| 89 | + exit 0 |
| 90 | +fi |
| 91 | + |
| 92 | +if ! [[ "$tokens" =~ ^[0-9]+$ ]]; then |
| 93 | + printf "Warning: invalid tokens parameter: %s\n" "$tokens" >&2 |
| 94 | + exit 1 |
| 95 | +fi |
| 96 | + |
| 97 | +if (( tokens > MAX_TOKENS )); then |
| 98 | + log "BLOCKED: Context7 call with $tokens tokens (limit: $MAX_TOKENS)" |
| 99 | + cat >&2 <<EOWARN |
| 100 | +Context7 token limit exceeded: $tokens > $MAX_TOKENS |
| 101 | +
|
| 102 | +Keep queries iterative so each pull stays focused. Reduce the topic scope and re-run at <= $MAX_TOKENS tokens. |
| 103 | +EOWARN |
| 104 | + exit 2 |
| 105 | +fi |
| 106 | + |
| 107 | +exit 0 |
| 108 | +``` |
| 109 | + |
| 110 | +**`~/.factory/hooks/context7_archive.sh`** |
| 111 | + |
| 112 | +```bash |
| 113 | +#!/usr/bin/env bash |
| 114 | +set -euo pipefail |
| 115 | +umask 077 |
| 116 | + |
| 117 | +ARCHIVE_DIR="${ARCHIVE_DIR:-${FACTORY_PROJECT_DIR:-$PWD}/context7-archive}" |
| 118 | +MAX_TOPIC_LENGTH=${MAX_TOPIC_LENGTH:-50} |
| 119 | +DEBUG_LOG="$ARCHIVE_DIR/hook-debug.log" |
| 120 | +RAW_JSON="${RAW_JSON:-0}" |
| 121 | + |
| 122 | +debug() { |
| 123 | + if [[ "${DEBUG:-0}" == "1" ]]; then |
| 124 | + mkdir -p "$ARCHIVE_DIR" |
| 125 | + echo "[$(date -u +"%Y-%m-%dT%H:%M:%SZ")] $*" >> "$DEBUG_LOG" |
| 126 | + fi |
| 127 | +} |
| 128 | + |
| 129 | +if ! command -v jq >/dev/null 2>&1; then |
| 130 | + echo "Warning: jq not found. Context7 archive hook disabled." >&2 |
| 131 | + exit 0 |
| 132 | +fi |
| 133 | + |
| 134 | +payload="$(cat)" |
| 135 | +tool_name="$(printf "%s" "$payload" | jq -r '.tool_name // empty' 2>/dev/null || printf "")" |
| 136 | + |
| 137 | +if [[ "$tool_name" != "context7___get-library-docs" ]]; then |
| 138 | + debug "Skipping non-Context7 tool" |
| 139 | + exit 0 |
| 140 | +fi |
| 141 | + |
| 142 | +if [[ "$RAW_JSON" == "1" ]]; then |
| 143 | + results="$(printf "%s" "$payload" | jq -c '.tool_response' 2>/dev/null || printf "")" |
| 144 | +else |
| 145 | + results="$(printf "%s" "$payload" | jq -r '.tool_response | if type == "string" then . else (.text // tojson) end' 2>/dev/null || printf "")" |
| 146 | +fi |
| 147 | + |
| 148 | +if [[ -z "$results" || "$results" == "null" ]]; then |
| 149 | + debug "No results to archive" |
| 150 | + exit 0 |
| 151 | +fi |
| 152 | + |
| 153 | +library_id="$(printf "%s" "$payload" | jq -r '.tool_input.context7CompatibleLibraryID // "unknown-library"' 2>/dev/null || printf "unknown-library")" |
| 154 | +topic="$(printf "%s" "$payload" | jq -r '.tool_input.topic // "untitled"' 2>/dev/null || printf "untitled")" |
| 155 | + |
| 156 | +project_name="$(basename "${FACTORY_PROJECT_DIR:-$PWD}" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')" |
| 157 | +library_slug="$(printf "%s" "$library_id" | sed 's|^/||' | cut -d/ -f2 | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')" |
| 158 | +library_slug="${library_slug:-unknown}" |
| 159 | +topic_slug="$(printf "%s" "$topic" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ */-/g' | sed 's/^-//' | sed 's/-$//')" |
| 160 | +topic_slug="${topic_slug:-untitled}" |
| 161 | + |
| 162 | +if [[ ${#topic_slug} -gt $MAX_TOPIC_LENGTH ]]; then |
| 163 | + topic_slug="${topic_slug:0:$MAX_TOPIC_LENGTH}" |
| 164 | + topic_slug="${topic_slug%-*}" |
| 165 | +fi |
| 166 | + |
| 167 | +mkdir -p "$ARCHIVE_DIR" |
| 168 | +stamp="$(date +"%Y%m%d")" |
| 169 | +base="${stamp}_${project_name}_${library_slug}_${topic_slug}" |
| 170 | +file="$ARCHIVE_DIR/${base}.md" |
| 171 | +counter=1 |
| 172 | +while [[ -f "$file" ]]; do |
| 173 | + file="$ARCHIVE_DIR/${base}-${counter}.md" |
| 174 | + ((counter++)) |
| 175 | +done |
| 176 | + |
| 177 | +cat > "$file" <<EOC |
| 178 | +--- |
| 179 | +query_date: $(date -u +"%Y-%m-%d %H:%M:%S UTC") |
| 180 | +library: $library_id |
| 181 | +topic: $topic |
| 182 | +project: $project_name |
| 183 | +tool: $tool_name |
| 184 | +--- |
| 185 | +
|
| 186 | +# Context7 Query: $topic |
| 187 | +
|
| 188 | +$results |
| 189 | +EOC |
| 190 | + |
| 191 | +debug "Archived to $file" |
| 192 | +exit 0 |
| 193 | +``` |
| 194 | + |
| 195 | +Then make both scripts executable: |
| 196 | + |
| 197 | +```bash |
| 198 | +chmod +x ~/.factory/hooks/context7_token_limiter.sh ~/.factory/hooks/context7_archive.sh |
| 199 | +``` |
| 200 | + |
| 201 | +<Note> |
| 202 | + The matcher should target the LLM tool name `context7___get-library-docs`. If you’re unsure, inspect `~/.factory/hooks/context7.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. |
| 203 | +</Note> |
| 204 | + |
| 205 | +## Step 3 · Token limiter (PreToolUse) |
| 206 | + |
| 207 | +- **Hook file:** `~/.factory/hooks/context7_token_limiter.sh` |
| 208 | +- **Purpose:** Block any `context7___get-library-docs` call that requests more than 3,000 tokens. |
| 209 | +- **Useful env vars:** |
| 210 | + |
| 211 | +```bash |
| 212 | +export MAX_TOKENS=3000 |
| 213 | +export LOG_FILE="$HOME/.factory/hooks/context7.log" # Optional auditing |
| 214 | +``` |
| 215 | + |
| 216 | +When the script exits with code 2, Factory halts the tool call and surfaces the warning text to the assistant. |
| 217 | + |
| 218 | +## Step 4 · Archive writer (PostToolUse) |
| 219 | + |
| 220 | +- **Hook file:** `~/.factory/hooks/context7_archive.sh` |
| 221 | +- **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). |
| 222 | +- **Useful env vars:** |
| 223 | + |
| 224 | +```bash |
| 225 | +export DEBUG=1 # Verbose logging to hook-debug.log |
| 226 | +export ARCHIVE_DIR="$HOME/context7-history" # Optional custom location |
| 227 | +export RAW_JSON=1 # Store raw JSON payloads instead of rendered text |
| 228 | +``` |
| 229 | + |
| 230 | +Each file includes YAML frontmatter so you can grep or index entries later (e.g., `20251114_myapp_nextjs_server-actions.md`). |
| 231 | + |
| 232 | +## Step 5 · Register the hooks |
| 233 | + |
| 234 | +You can register these hooks either through the `/hooks` UI (recommended) or by editing `~/.factory/settings.json` directly. |
| 235 | + |
| 236 | +### Option A - Use the Hooks UI |
| 237 | + |
| 238 | +1. Run `/settings` and make sure **Hooks** is set to **Enabled**. |
| 239 | +2. Run `/hooks`, select the **PreToolUse** event, and add a matcher `context7___get-library-docs`. Hit enter to save. |
| 240 | +3. Add a `command`: `~/.factory/hooks/context7_token_limiter.sh`, and store it in **User settings**. |
| 241 | +4. Repeat for **PostToolUse**, matcher `context7___get-library-docs`, command `~/.factory/hooks/context7_archive.sh`. |
| 242 | + |
| 243 | +### Option B - Edit settings JSON |
| 244 | + |
| 245 | +Open `~/.factory/settings.json` and add a `hooks` block like this (merging with any existing hooks): |
| 246 | + |
| 247 | +```jsonc |
| 248 | +{ |
| 249 | + "hooks": { |
| 250 | + "PreToolUse": [ |
| 251 | + { |
| 252 | + "matcher": "context7___get-library-docs", |
| 253 | + "hooks": [ |
| 254 | + { |
| 255 | + "type": "command", |
| 256 | + "command": "~/.factory/hooks/context7_token_limiter.sh", |
| 257 | + "timeout": 5000 |
| 258 | + } |
| 259 | + ] |
| 260 | + } |
| 261 | + ], |
| 262 | + "PostToolUse": [ |
| 263 | + { |
| 264 | + "matcher": "context7___get-library-docs", |
| 265 | + "hooks": [ |
| 266 | + { |
| 267 | + "type": "command", |
| 268 | + "command": "~/.factory/hooks/context7_archive.sh", |
| 269 | + "timeout": 5000 |
| 270 | + } |
| 271 | + ] |
| 272 | + } |
| 273 | + ] |
| 274 | + } |
| 275 | +} |
| 276 | +``` |
| 277 | + |
| 278 | +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. |
| 279 | + |
| 280 | +Restart Factory (or reopen your session) after editing your hooks configuration. |
| 281 | + |
| 282 | +## Step 6 · Test the workflow |
| 283 | + |
| 284 | +<Steps> |
| 285 | + <Step title="Limiter"> |
| 286 | + 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.) |
| 287 | + </Step> |
| 288 | + <Step title="Archive"> |
| 289 | + Run a normal Context7 request. Confirm `context7-archive/` now contains a timestamped Markdown file with the query results. |
| 290 | + </Step> |
| 291 | +</Steps> |
| 292 | + |
| 293 | +## Troubleshooting & customization |
| 294 | + |
| 295 | +- **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. |
| 296 | +- **Missing `jq`:** Install it with `brew install jq` (macOS) or your distro’s package manager. |
| 297 | +- **Permissions:** Ensure every script in `~/.factory/hooks` is executable (`chmod +x ~/.factory/hooks/*`). |
| 298 | +- **Archive clutter:** Add `context7-archive/` to `.gitignore` if you don’t plan to commit the saved docs. |
| 299 | +- **Timeouts:** Increase the `timeout` field in `hooks.json` if you routinely archive very large responses. |
| 300 | + |
| 301 | +With these two hooks in place, every Context7 pull stays within a predictable token budget and automatically lands in a searchable knowledge base. |
0 commit comments