skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
fellowship-dev/dogfooded-skills168 installs

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 hookshot
view source ↗

Is 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.

FlagWhat 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-warnSkill 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-lintMarkdown lint — PostToolUse Edit/Write hook that runs npx markdownlint-cli2 on any changed *.md file and surfaces warnings. Never auto-fixes.
--preflightSession 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.
--gateDelivery 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-harness creates the knowledge layer — hookshot wires it to the agent runtime
  • After updating docs/code-structure.md or docs/code-guidelines.md — regenerate hooks to stay current
  • When the #1585-class bug occurs: 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.sh installs the hookshot skill via npx skills add. But installing the skill ≠ generating hooks. You must run /hookshot at least once to generate the artifacts (doc-coverage.json, check-docs.sh, settings.json hooks, 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-check monitors 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

  1. check-docs.sh — Given a file being edited, outputs a doc reminder to stderr if that file is covered by docs/
  2. .claude/settings.json hooks — PreToolUse hook calling check-docs.sh on every Edit/Write
  3. Domain coverage map — $REPO_ROOT/.claude/doc-coverage.json — maps file globs to doc sections
  4. 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 PreToolUse already exists → add to the array, don't replace
  • If an identical check-docs.sh hook 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 lines
  • MD033 (inline HTML) — off; we use HTML details/summary in reports
  • MD041 (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 per references/shadow-log.md, and in enforce mode returns {"decision": "block", "reason": …} with the unmet items. It never blocks while stop_hook_active is 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

VariableDefaultPurpose
HOOKSHOT_STATE_DIR<cwd>/.statepointers, shadow log, judge packets. Gitignore it.
HOOKSHOT_GATE_MODEshadowshadow logs; enforce refuses. Flip only after reading the log.
HOOKSHOT_PREFLIGHT_DOCunsetrepo-relative doc the preflight reminder tells the agent to read
HOOKSHOT_NO_NETWORKunsetskip 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:

PatternMatches
app/controllers/**/*.rbAny Ruby file under controllers/
src/pages/**/*.tsxAny TSX file under pages/
lib/redirect_service.rbExact file
app/models/user.rbExact file
**/*_mailer.rbAny mailer anywhere

Use specific globs for high-criticality files. Use broad globs for domain directories.

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>