Loading...
Loading...
Use when generating Claude Code enforcement hooks from a repo's docs/ structure.
npx skill4agent add fellowship-dev/dogfooded-skills hookshot| 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 |
/hookshot --drift-warn --md-lintnpx skills add fellowship-dev/dogfooded-skills/ops/hookshot/setup-harnessdocs/code-structure.mddocs/code-guidelines.md#1585-class bugboot-skills.shnpx skills add/hookshotdoc-coverage.jsoncheck-docs.shsettings.jsondocs/hooks.mdentropy-check/hookshot"Because the lints are custom, we write the error messages to inject remediation instructions into agent context." — OpenAI harness engineering
check_redirectcheck-docs.sh.claude/settings.jsoncheck-docs.sh$REPO_ROOT/.claude/doc-coverage.jsonREPO_ROOT=$(git rev-parse --show-toplevel)
REPO_NAME=$(basename $(git remote get-url origin) .git)
mkdir -p $REPO_ROOT/.claude# 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/nulldocs/code-structure.md.flowchad/flows/file:$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"
}
]
}criticality: "high"$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 0chmod +x $REPO_ROOT/.claude/check-docs.sh.claude/settings.json{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash {REPO_ROOT}/scripts/check-docs.sh \"$(jq -r '.tool_input.file_path // empty')\""
}
]
}
]
}
}PreToolUsecheck-docs.sh.claude/settings.jsondocs/code-guidelines.md$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}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
ficheck-docs.shdocs/hooks.md# 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.docs/hooks.md# 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"## 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--drift-warn.claude/skills/<name>/skills-lock.json.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.claude/settings.jsonPreToolUseEdit|Write# 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"--md-lintnpx markdownlint-cli2.md.markdownlint.json.markdownlint.json.markdownlint-cli2.jsonc{
"default": true,
"MD013": false,
"MD033": false,
"MD041": false
}MD013MD033MD041.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.claude/settings.jsonPostToolUseEdit|Write# 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.jsondoc-coverage.json| Pattern | Matches |
|---|---|
| Any Ruby file under controllers/ |
| Any TSX file under pages/ |
| Exact file |
| Exact file |
| Any mailer anywhere |