hookshot
Use when generating agent enforcement hooks for a repo — doc-coverage reminders from docs/, skill-drift and markdown-lint warnings, and the session preflight and delivery-gate hooks that check a session against its outcome contract. Installs into Claude Code and Codex hook configuration.
How do I install this agent skill?
npx skills add https://github.com/fellowship-dev/dogfooded-skills --skill hookshotIs this agent skill safe to install?
- Gen Agent Trust Hubwarn
The skill automates the creation and configuration of repository-specific hooks for AI agents. It generates shell scripts to check for documentation coverage and potential skill drift, and integrates these into the agent's runtime environment by modifying settings.json.
- Socketwarn
1 alert: gptAnomaly
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
Hookshot
From docs to enforcement hooks. Reads what you've documented. Generates the hooks that make agents read it.
Philosophy: Hookshot warns by default, blocks only for a gate the repo opted into. It never amends files, never lints-and-fixes. Every hook it generates emits guidance and exits 0; the one exception is the delivery gate, which a repo can flip from shadow (log only) to enforce (refuse the stop) after reading its own shadow log.
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 docs/ section before editing a covered file. (Default behavior, documented below.) |
--drift-warn | Skill drift warning — PreToolUse Edit/Write hook that warns when an agent edits a file inside .claude/skills/<name>/ for a skill that's tracked in skills-lock.json. Edits should go upstream. |
--md-lint | Markdown lint — PostToolUse Edit/Write hook that runs npx markdownlint-cli2 on any changed *.md file and surfaces warnings. Never auto-fixes. |
--preflight | Session preflight — SessionStart hook (startup, resume, clear, compact) that tells the agent its session id, to run preflight for a task, and where to record its outcome-contract pointer. Ships scripts/preflight.py. |
--gate | Delivery gate — Stop hook that diffs what exists against the session's outcome contract. Shadow mode logs would-be refusals; enforce mode refuses the stop. Ships scripts/gate.py, references/contract-format.md, references/shadow-log.md. |
Flags compose: /hookshot --drift-warn --md-lint installs both new hooks alongside the default doc-coverage hook. --preflight --gate are the only modes that ship scripts of their own and the only ones that install into Codex as well as Claude Code. Re-running hookshot merges with existing hooks idempotently.
Install via npx:
npx skills add fellowship-dev/dogfooded-skills/ops/hookshot
When to Use
- After
/setup-harnesscreates the knowledge layer — hookshot wires it to the agent runtime - After updating
docs/code-structure.mdordocs/code-guidelines.md— regenerate hooks to stay current - When the
#1585-class bugoccurs: agent modified a critical path without reading docs — add a hook to prevent recurrence - After adding a new FlowChad flow — generate hooks for that critical path
Integration with Pylot
- Installation:
boot-skills.shinstalls the hookshot skill vianpx skills add. But installing the skill ≠ generating hooks. You must run/hookshotat least once to generate the artifacts (doc-coverage.json,check-docs.sh,settings.jsonhooks,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:
entropy-checkmonitors hookshot coverage freshness on PR merge and weekly cron. When it flags staleness, re-run/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 check_redirect
works before modifying this area" — this skill generates that message from your docs.
What It Generates
check-docs.sh— Given a file being edited, outputs a doc reminder to stderr if that file is covered by docs/.claude/settings.jsonhooks — PreToolUse hook callingcheck-docs.shon every Edit/Write- Domain coverage map —
$REPO_ROOT/.claude/doc-coverage.json— maps file globs to doc sections - Custom lint messages — Remediation instructions with specific doc section links
Instructions
0. Identify the Repo
REPO_ROOT=$(git rev-parse --show-toplevel)
REPO_NAME=$(basename $(git remote get-url origin) .git)
mkdir -p $REPO_ROOT/.claude
1. Parse Knowledge Layer
Read all documentation files and extract coverage mappings:
# 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/null
For each domain section in docs/code-structure.md, extract:
- 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
file:entries in the flow)
2. Build Coverage Map
Create $REPO_ROOT/.claude/doc-coverage.json:
{
"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.sh:
#!/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 0
Make it executable:
chmod +x $REPO_ROOT/.claude/check-docs.sh
4. Write Hooks to settings.json
Read the existing .claude/settings.json if it exists.
Merge in the hooks configuration — do not clobber existing hooks.
The hook to add:
{
"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
PreToolUsealready exists → add to the array, don't replace - If an identical
check-docs.shhook already exists → skip (idempotent) - Preserve all existing hook entries
Write the merged result back to .claude/settings.json.
5. Generate Custom Lint Messages
For each guideline in docs/code-guidelines.md, generate a lint message file:
Create $REPO_ROOT/.claude/lint-messages.md:
# 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.sh:
# 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
fi
Add these rule-based checks to check-docs.sh in a clearly marked section.
5b. Generate docs/hooks.md
Generate a docs/hooks.md file in the target repo documenting the active hooks:
# 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 docs/hooks.md and merge: preserve any human-added sections, regenerate the "Covered Domains" table and "Active Hooks" section from current state.
6. Verification
Test the generated hook:
# 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 coverage
Mode: Drift Warning (--drift-warn)
Warns when an agent is about to edit a file inside a .claude/skills/<name>/ dir for a skill that's tracked in skills-lock.json. The actual edit is not blocked — this is guidance, and agents sometimes legitimately need to hotfix a synced skill before upstreaming.
Generate .claude/check-skill-drift.sh
#!/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 0
Make it executable and wire into .claude/settings.json under PreToolUse with matcher Edit|Write. Merge-don't-clobber, same strategy as the default doc-coverage hook.
Verification
# 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)
Runs npx markdownlint-cli2 on any changed .md file after an Edit or Write, and surfaces the warnings to the agent. Never auto-fixes — agent decides.
Starter .markdownlint.json
If the repo has no .markdownlint.json or .markdownlint-cli2.jsonc at root, drop a permissive starter so the linter isn't overwhelming out of the box:
{
"default": true,
"MD013": false,
"MD033": false,
"MD041": false
}
MD013(line length) — off by default; docs and skill files often have long linesMD033(inline HTML) — off; we use HTML details/summary in reportsMD041(first line must be h1) — off; many docs start with frontmatter
If a config already exists, leave it. Never overwrite.
Generate .claude/check-md-lint.sh
#!/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 0
Wire into .claude/settings.json under PostToolUse (not PreToolUse — the file must exist before it can be linted) with matcher Edit|Write.
Verification
# 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.json
Mode: Session preflight and delivery gate (--preflight, --gate)
Two hooks, one owner. The scripts live in this skill (scripts/preflight.py, scripts/gate.py, Python 3 standard library) and the client configuration only points at them. A repo never carries its own copy of the logic; it carries wiring.
- Preflight (SessionStart on startup, resume, clear, compact): injects the session id, a reminder to run preflight when the first prompt is a task, and the pointer path
<state>/contracts/<session_id>where the agent records its contract's repo-relative path. Creates<state>/contracts/. - Gate (Stop): finds the contract through that pointer, checks it per
references/contract-format.md, appends a line to the shadow log perreferences/shadow-log.md, and in enforce mode returns{"decision": "block", "reason": …}with the unmet items. It never blocks whilestop_hook_activeis true, and exits 0 on every internal error.
Both clients pass session_id, cwd, source (SessionStart) and stop_hook_active, last_assistant_message (Stop) on stdin and accept the same additionalContext and decision: block shapes. Codex parses and skips prompt/agent handlers, so an LLM judge is native to Claude Code only; the gate records judge: none on Codex.
Environment
| Variable | Default | Purpose |
|---|---|---|
HOOKSHOT_STATE_DIR | <cwd>/.state | pointers, shadow log, judge packets. Gitignore it. |
HOOKSHOT_GATE_MODE | shadow | shadow logs; enforce refuses. Flip only after reading the log. |
HOOKSHOT_PREFLIGHT_DOC | unset | repo-relative doc the preflight reminder tells the agent to read |
HOOKSHOT_NO_NETWORK | unset | skip URL and PR checks (they record unverifiable) |
Install into Claude Code (.claude/settings.json, project level)
{
"hooks": {
"SessionStart": [{"hooks": [{"type": "command",
"command": "HOOKSHOT_PREFLIGHT_DOC=docs/preflight.md python3 \"$CLAUDE_PROJECT_DIR/.agents/skills/hookshot/scripts/preflight.py\" --client claude-code"}]}],
"Stop": [{"hooks": [{"type": "command", "timeout": 20,
"command": "HOOKSHOT_GATE_MODE=shadow python3 \"$CLAUDE_PROJECT_DIR/.agents/skills/hookshot/scripts/gate.py\" --client claude-code"}]}]
}
}
Omit the SessionStart matcher so it fires on every source. Claude Code desktop and the VS Code extension read the same file. Merge, don't clobber, as with the other modes.
Install into Codex (.codex/hooks.json, project level)
{
"hooks": {
"SessionStart": [{"hooks": [{"type": "command",
"command": "HOOKSHOT_PREFLIGHT_DOC=docs/preflight.md python3 .agents/skills/hookshot/scripts/preflight.py --client codex"}]}],
"Stop": [{"hooks": [{"type": "command", "timeout": 20,
"command": "HOOKSHOT_GATE_MODE=shadow python3 .agents/skills/hookshot/scripts/gate.py --client codex"}]}]
}
}
Codex runs hooks from the session cwd. Codex trusts each hook definition by hash: after installing or changing a hook, a person runs /hooks in an interactive Codex session on each machine and trusts it. Do not automate that with --dangerously-bypass-hook-trust. Codex Desktop and the IDE extension have open issues about hooks not firing and Stop blocks erroring; treat a wrap-up step as the gate there until a shadow log proves otherwise.
Non-interactive sessions
Where a client does not fire hooks (for example a runner that starts codex exec with user config ignored), the runner calls the same script after the session ends:
python3 .agents/skills/hookshot/scripts/gate.py --client codex-exec \
--session-id "$RUN_ID" --last-message "$LAST_MESSAGE_FILE" --cwd "$REPO"
The contract pointer is still <state>/contracts/<RUN_ID>; pass --contract PATH when the runner already knows it.
Verification
python3 .agents/skills/hookshot/scripts/test_gate.py
echo '{"session_id":"t","cwd":"'$PWD'","source":"startup"}' | python3 .agents/skills/hookshot/scripts/preflight.py --client claude-code
echo '{"session_id":"t","cwd":"'$PWD'","stop_hook_active":false,"last_assistant_message":"DELIVERED"}' | python3 .agents/skills/hookshot/scripts/gate.py --client codex
tail -1 .state/hookshot/gate-shadow.jsonl
Coverage Map Reference
The doc-coverage.json format supports these glob styles:
| Pattern | Matches |
|---|---|
app/controllers/**/*.rb | Any Ruby file under controllers/ |
src/pages/**/*.tsx | Any TSX file under pages/ |
lib/redirect_service.rb | Exact file |
app/models/user.rb | Exact file |
**/*_mailer.rb | Any mailer anywhere |
Use specific globs for high-criticality files. Use broad globs for domain directories.
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/fellowship-dev/dogfooded-skills/hookshot">View hookshot on skillZs</a>