hookshot
Original:🇺🇸 English
Translated
Use when generating Claude Code enforcement hooks from a repo's docs/ structure.
6installs
Added on
NPX Install
npx skill4agent add fellowship-dev/dogfooded-skills hookshotTags
Translated version includes tags in frontmatterSKILL.md Content
View Translation Comparison →Hookshot
From docs to enforcement hooks. Reads what you've documented. Generates the hooks that make agents read it.
Philosophy: Hookshot only issues warnings and guidance. It never amends files, never lints-and-fixes. All hooks it generates emit messages to stderr and exit 0 — the agent decides whether to act.
Modes
Hookshot is composable — invoke with one or more flags. Default mode (no flags) runs the doc-coverage generator.
| Flag | What it adds |
|---|---|
| (no flag) | Doc coverage — PreToolUse Edit/Write hook that nudges agents to read the relevant |
| Skill drift warning — PreToolUse Edit/Write hook that warns when an agent edits a file inside |
| Markdown lint — PostToolUse Edit/Write hook that runs |
Flags compose: installs both new hooks alongside the default doc-coverage hook. Re-running hookshot merges with existing hooks idempotently.
/hookshot --drift-warn --md-lintInstall via npx:
bash
npx skills add fellowship-dev/dogfooded-skills/ops/hookshotWhen to Use
- After creates the knowledge layer — hookshot wires it to the agent runtime
/setup-harness - After updating or
docs/code-structure.md— regenerate hooks to stay currentdocs/code-guidelines.md - When the occurs: agent modified a critical path without reading docs — add a hook to prevent recurrence
#1585-class bug - After adding a new FlowChad flow — generate hooks for that critical path
Integration with Pylot
- Installation: installs the hookshot skill via
boot-skills.sh. But installing the skill ≠ generating hooks. You must runnpx skills addat least once to generate the artifacts (/hookshot,doc-coverage.json,check-docs.shhooks,settings.json).docs/hooks.md - setup-harness: Runs hookshot as its final phase on first setup. You don't need to run hookshot separately after setup-harness.
- Staleness: monitors hookshot coverage freshness on PR merge and weekly cron. When it flags staleness, re-run
entropy-check./hookshot - Per-repo: Each repo gets its own hooks. Cross-repo missions use the target repo's hooks.
Key Insight
"Because the lints are custom, we write the error messages to inject remediation instructions into agent context." — OpenAI harness engineering
Hookshot makes this automatic. The agent would have been told "read how
works before modifying this area" — this skill generates that message from your docs.
check_redirectWhat It Generates
- — Given a file being edited, outputs a doc reminder to stderr if that file is covered by docs/
check-docs.sh - hooks — PreToolUse hook calling
.claude/settings.jsonon every Edit/Writecheck-docs.sh - Domain coverage map — — maps file globs to doc sections
$REPO_ROOT/.claude/doc-coverage.json - Custom lint messages — Remediation instructions with specific doc section links
Instructions
0. Identify the Repo
bash
REPO_ROOT=$(git rev-parse --show-toplevel)
REPO_NAME=$(basename $(git remote get-url origin) .git)
mkdir -p $REPO_ROOT/.claude1. Parse Knowledge Layer
Read all documentation files and extract coverage mappings:
bash
# Read all key docs
cat $REPO_ROOT/docs/code-structure.md 2>/dev/null
cat $REPO_ROOT/docs/code-guidelines.md 2>/dev/null
cat $REPO_ROOT/ARCHITECTURE.md 2>/dev/null
ls $REPO_ROOT/.flowchad/flows/ 2>/dev/nullFor each domain section in , extract:
docs/code-structure.md- Domain name (section header)
- Directory path (the "Directory:" line)
- Key files (from the Entry Points table)
- Critical patterns ("Don't Repeat" section — these are the highest priority)
For each FlowChad flow in :
.flowchad/flows/- Flow name
- Domain
- Entry point file (from the flow definition)
- Files touched (all entries in the flow)
file:
2. Build Coverage Map
Create :
$REPO_ROOT/.claude/doc-coverage.jsonjson
{
"version": "1",
"generated": "{DATE}",
"repo": "{REPO_NAME}",
"entries": [
{
"glob": "app/controllers/**/*.rb",
"domain": "Controllers",
"doc_section": "docs/code-structure.md#controllers",
"reminder": "Before modifying a controller, read how the controller pattern works: docs/code-structure.md#controllers. Key rule: controllers do not query the DB directly — use service objects.",
"criticality": "high"
},
{
"glob": "app/services/**/*.rb",
"domain": "Services",
"doc_section": "docs/code-structure.md#services",
"reminder": "Service objects in app/services/ follow the Command pattern. Read docs/code-structure.md#services for the interface contract.",
"criticality": "medium"
}
]
}Build one entry per domain directory mapping. For critical paths (found in FlowChad flows), set .
criticality: "high"Reminder text rules:
- Lead with the specific doc section to read
- Include the most important "Don't Repeat" rule for that domain
- Keep under 200 characters — this appears in agent context, not a wall of text
- Be actionable: "Read X" not "Consider reading X"
3. Generate check-docs.sh
Write :
$REPO_ROOT/.claude/check-docs.shbash
#!/usr/bin/env bash
# check-docs.sh — Generated by hookshot on {DATE}
# Usage: check-docs.sh <file_path_being_edited>
# Outputs doc reminders to stderr if the file is covered by docs/
set -euo pipefail
FILE_PATH="${1:-}"
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# Normalize path relative to repo root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
COVERAGE_MAP="$SCRIPT_DIR/doc-coverage.json"
if [ ! -f "$COVERAGE_MAP" ]; then
exit 0
fi
# Check file against each glob in the coverage map
# Uses jq to parse coverage map and bash glob matching
REMINDERS=$(jq -r '.entries[] | "\(.glob)\t\(.reminder)\t\(.criticality)"' "$COVERAGE_MAP" 2>/dev/null)
FOUND_REMINDER=""
FOUND_CRITICALITY=""
while IFS=$'\t' read -r GLOB REMINDER CRITICALITY; do
# Normalize the file path
REL_PATH="${FILE_PATH#$REPO_ROOT/}"
# Normalize for bash [[ ]] pattern matching:
# 1. **/ → * (** has no special meaning; * already matches any char incl /)
# 2. Escape [ ] so Next.js routes like [locale] are literal, not char classes
GLOB="${GLOB//\*\*\//*}"
GLOB="${GLOB//\[/\\[}"
GLOB="${GLOB//\]/\\]}"
if [[ "$REL_PATH" == $GLOB ]]; then
FOUND_REMINDER="$REMINDER"
FOUND_CRITICALITY="$CRITICALITY"
break
fi
done <<< "$REMINDERS"
if [ -n "$FOUND_REMINDER" ]; then
if [ "$FOUND_CRITICALITY" = "high" ]; then
echo "⚠️ DOCUMENTATION REMINDER (high criticality)" >&2
echo "$FOUND_REMINDER" >&2
echo "" >&2
echo "This file is in a critical path. Read the doc section before proceeding." >&2
else
echo "📖 Doc reminder: $FOUND_REMINDER" >&2
fi
fi
exit 0Make it executable:
bash
chmod +x $REPO_ROOT/.claude/check-docs.sh4. Write Hooks to settings.json
Read the existing if it exists.
Merge in the hooks configuration — do not clobber existing hooks.
.claude/settings.jsonThe hook to add:
json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash {REPO_ROOT}/scripts/check-docs.sh \"$(jq -r '.tool_input.file_path // empty')\""
}
]
}
]
}
}Merge strategy:
- If already exists → add to the array, don't replace
PreToolUse - If an identical hook already exists → skip (idempotent)
check-docs.sh - Preserve all existing hook entries
Write the merged result back to .
.claude/settings.json5. Generate Custom Lint Messages
For each guideline in , generate a lint message file:
docs/code-guidelines.mdCreate :
$REPO_ROOT/.claude/lint-messages.mdmarkdown
# Custom Lint Messages — {REPO_NAME}
Generated by hookshot on {DATE}. Used by PreToolUse hooks to inject remediation context.
## {Domain}: {Rule Name}
**Pattern detected:** {what triggers this message}
**Message injected into context:**
> {The exact message the agent will see}
**Doc reference:** docs/code-guidelines.md#{anchor}
---
{Repeat per rule}For critical rules (e.g., "never roll your own auth", "use check_redirect not inline conditionals"), generate explicit check commands to add to :
check-docs.shbash
# Add to check-docs.sh after the glob check:
# Rule-based checks (pattern detection in file content)
if echo "$FILE_PATH" | grep -q "controllers/"; then
# Check if file being written contains a raw redirect without check_redirect
# (This is a hint — actual content checking happens post-edit)
echo "📖 Controllers reminder: Use check_redirect in lib/redirect_service.rb for all redirects." >&2
fiAdd these rule-based checks to in a clearly marked section.
check-docs.sh5b. Generate docs/hooks.md
Generate a file in the target repo documenting the active hooks:
docs/hooks.mdmarkdown
# Hooks — {REPO_NAME}
> Auto-generated by [hookshot](https://github.com/fellowship-dev/dogfooded-skills). Safe to add notes — hookshot merges on update, it won't overwrite your additions.
## Active Hooks
### PreToolUse: Doc Reminders on Edit/Write
**Trigger:** Every `Edit` or `Write` tool call
**Script:** `scripts/check-docs.sh` (or `.claude/check-docs.sh`)
**Config:** `.claude/settings.json` → `hooks.PreToolUse`
**Coverage map:** `.claude/doc-coverage.json`
When an agent edits a file matching a covered glob, the hook injects a doc reminder into context before the edit proceeds. High-criticality files produce warnings; medium-criticality files produce reminders.
## Covered Domains
{For each entry in doc-coverage.json, list:}
| Domain | Glob | Criticality | Reminder |
|--------|------|-------------|----------|
| {domain} | `{glob}` | {criticality} | {reminder} |
## Maintaining Hooks
- **Quick tweaks:** Edit `.claude/doc-coverage.json` directly — add/remove entries, adjust criticality or reminder text. Changes take effect immediately.
- **Full regeneration:** Run `/hookshot` to rebuild coverage map from current `docs/code-structure.md`. This merges with your existing `doc-coverage.json` and `docs/hooks.md` — it won't overwrite manual additions.
- **Staleness detection:** `/entropy-check` monitors whether hooks are current vs docs. If it flags staleness, re-run `/hookshot`.
## Troubleshooting
### Glob doesn't match expected files
The hook uses bash `[[ ]]` pattern matching, which differs from gitignore globs:
- `**` has no special meaning — `*` already matches any character including `/`
- `[brackets]` are character classes, not literal — Next.js routes like `[locale]` need escaping
- The generated `check-docs.sh` normalizes both automatically. If you're writing manual globs, use `*` not `**/*` for recursive matching.
Test a glob: `bash scripts/check-docs.sh "/full/path/to/file.ts"`
### Hook breaks the agent or slows edits
Disable temporarily by removing the hook entry from `.claude/settings.json`. Re-run `/hookshot` to restore.
### settings.json got clobbered
Re-run `/hookshot` — it merges hooks into existing settings, never overwrites other config.On re-runs, read the existing and merge: preserve any human-added sections, regenerate the "Covered Domains" table and "Active Hooks" section from current state.
docs/hooks.md6. Verification
Test the generated hook:
bash
# Test with a file that should trigger a reminder
bash $REPO_ROOT/.claude/check-docs.sh "$REPO_ROOT/app/controllers/sessions_controller.rb"
# Test with a file that should NOT trigger
bash $REPO_ROOT/.claude/check-docs.sh "$REPO_ROOT/README.md"
# Verify settings.json is valid JSON
cat $REPO_ROOT/.claude/settings.json | jq . > /dev/null && echo "settings.json: valid JSON"
# Verify doc-coverage.json is valid JSON
cat $REPO_ROOT/.claude/doc-coverage.json | jq . > /dev/null && echo "doc-coverage.json: valid JSON"7. Summary Report
## Hookshot Complete: {REPO_NAME}
### Coverage Map
- {N} domain entries in .claude/doc-coverage.json
- {N} high-criticality entries (will produce warnings)
- {N} medium-criticality entries (will produce reminders)
### Hooks Generated
- .claude/check-docs.sh — glob-based doc lookup
- .claude/settings.json — PreToolUse hook wired
- .claude/lint-messages.md — custom lint message catalog
- docs/hooks.md — human-readable hook documentation
### Coverage Gaps
{List any domains in ARCHITECTURE.md that have no glob coverage — need manual mapping}
### Manual Next Steps
- [ ] Review .claude/doc-coverage.json — adjust globs that are too broad or too narrow
- [ ] Test a real edit to a covered file and confirm the reminder appears
- [ ] Add rule-based checks for your most critical "Don't Repeat" patterns
- [ ] Run /entropy-check to verify grades reflect the new hook coverageMode: Drift Warning (--drift-warn
)
--drift-warnWarns when an agent is about to edit a file inside a dir for a skill that's tracked in . The actual edit is not blocked — this is guidance, and agents sometimes legitimately need to hotfix a synced skill before upstreaming.
.claude/skills/<name>/skills-lock.jsonGenerate .claude/check-skill-drift.sh
.claude/check-skill-drift.shbash
#!/usr/bin/env bash
# check-skill-drift.sh — Generated by hookshot (--drift-warn) on {DATE}
# Usage: check-skill-drift.sh <file_path>
# Warns to stderr if the file belongs to a skill tracked in skills-lock.json.
set -uo pipefail
FILE_PATH="${1:-}"
[ -z "$FILE_PATH" ] && exit 0
# Walk up from the file to find the nearest skills-lock.json
DIR="$(dirname "$FILE_PATH")"
LOCK_FILE=""
while [ "$DIR" != "/" ] && [ "$DIR" != "." ]; do
if [ -f "$DIR/skills-lock.json" ]; then
LOCK_FILE="$DIR/skills-lock.json"
break
fi
DIR="$(dirname "$DIR")"
done
[ -z "$LOCK_FILE" ] && exit 0
# Path must contain /.claude/skills/<name>/ or /.agents/skills/<name>/
SKILL_NAME="$(echo "$FILE_PATH" | sed -nE 's|.*/\.(claude|agents)/skills/([^/]+)/.*|\2|p')"
[ -z "$SKILL_NAME" ] && exit 0
# Look up in lockfile
SOURCE=$(python3 -c "
import json, sys
try:
data = json.load(open('$LOCK_FILE'))
entry = (data.get('skills') or {}).get('$SKILL_NAME')
if entry:
print(entry.get('source', ''))
except Exception:
pass
" 2>/dev/null)
if [ -n "$SOURCE" ]; then
echo "⚠️ SKILL DRIFT WARNING" >&2
echo "'$SKILL_NAME' is a remote skill synced from: $SOURCE" >&2
echo "Local edits will drift from upstream and may be overwritten on next 'npx skills update'." >&2
echo "Edit upstream at https://github.com/$SOURCE instead, or be prepared to PR the change back." >&2
fi
exit 0Make it executable and wire into under with matcher . Merge-don't-clobber, same strategy as the default doc-coverage hook.
.claude/settings.jsonPreToolUseEdit|WriteVerification
bash
# Should warn — cto-review is a tracked skill
bash .claude/check-skill-drift.sh "$PWD/.claude/skills/cto-review/SKILL.md"
# Should be silent — file is outside any skills dir
bash .claude/check-skill-drift.sh "$PWD/README.md"
# Should be silent — skill isn't in lockfile (e.g. a local-only skill)
bash .claude/check-skill-drift.sh "$PWD/.claude/skills/local-thing/SKILL.md"Mode: Markdown Lint (--md-lint
)
--md-lintRuns on any changed file after an Edit or Write, and surfaces the warnings to the agent. Never auto-fixes — agent decides.
npx markdownlint-cli2.mdStarter .markdownlint.json
.markdownlint.jsonIf the repo has no or at root, drop a permissive starter so the linter isn't overwhelming out of the box:
.markdownlint.json.markdownlint-cli2.jsoncjson
{
"default": true,
"MD013": false,
"MD033": false,
"MD041": false
}- (line length) — off by default; docs and skill files often have long lines
MD013 - (inline HTML) — off; we use HTML details/summary in reports
MD033 - (first line must be h1) — off; many docs start with frontmatter
MD041
If a config already exists, leave it. Never overwrite.
Generate .claude/check-md-lint.sh
.claude/check-md-lint.shbash
#!/usr/bin/env bash
# check-md-lint.sh — Generated by hookshot (--md-lint) on {DATE}
# Usage: check-md-lint.sh <file_path>
# Runs markdownlint-cli2 on the file if it's *.md. Warns only — never fixes.
set -uo pipefail
FILE_PATH="${1:-}"
[ -z "$FILE_PATH" ] && exit 0
# Only lint markdown files
case "$FILE_PATH" in
*.md|*.markdown) ;;
*) exit 0 ;;
esac
[ -f "$FILE_PATH" ] || exit 0
# Run markdownlint-cli2 — fast start via npx
OUTPUT=$(npx --yes markdownlint-cli2 "$FILE_PATH" 2>&1) || true
if [ -n "$OUTPUT" ] && echo "$OUTPUT" | grep -qE 'MD[0-9]{3}'; then
echo "📝 Markdown lint warnings for $(basename "$FILE_PATH"):" >&2
echo "$OUTPUT" | grep -E 'MD[0-9]{3}' | head -20 >&2
echo "(warnings only — no auto-fix. Run 'npx markdownlint-cli2 --fix <file>' manually if desired.)" >&2
fi
exit 0Wire into under (not PreToolUse — the file must exist before it can be linted) with matcher .
.claude/settings.jsonPostToolUseEdit|WriteVerification
bash
# Should print MD### warnings if the file has any lint issues
bash .claude/check-md-lint.sh README.md
# Should be silent — not a markdown file
bash .claude/check-md-lint.sh package.jsonCoverage Map Reference
The format supports these glob styles:
doc-coverage.json| Pattern | Matches |
|---|---|
| Any Ruby file under controllers/ |
| Any TSX file under pages/ |
| Exact file |
| Exact file |
| Any mailer anywhere |
Use specific globs for high-criticality files. Use broad globs for domain directories.