Install command
Provided
Validates all links in markdown files to detect broken links and references.
Open the source and read safety notes before installing.
Source-backed facts for citing this resource, derived directly from the registry — also available as plain text for AI assistants.
Decision playbook
Signals are present but mixed. Use the checklist below to confirm the source and operational safety for your environment.
0
78
—
No baseline selected
No major trust-signal divergence detected in the current selection.
Confirm ownership and provenance before trusting install instructions.
Source link availableRequired
Open the canonical repository and verify ownership.
Source provenance statusRequired
Marked as source-backed.
Metadata reviewed
Registry metadata indicates a reviewed listing.
Validate risk disclosures before installation or API wiring.
Safety notes presentRequired
Review the listed safety guidance before running commands.
Privacy notes presentRequired
Review data handling notes before connecting accounts or secrets.
Trust level risk gateRequired
Trust level does not block evaluation.
Check package metadata and artifact integrity signals.
Install payload available
Install or copy payload is available for review.
Package verification flag
No package verification flag provided.
Checksum metadata
No checksum provided for downloaded artifact.
Use compare context to validate trade-offs before adoption.
Compare tray has multiple entries
Add at least one more entry to compare trust differences.
Baseline comparison available
No baseline peer selected yet.
Diverging trust signals identified
No major trust-signal divergence found.
Setup at a glance
Copy-ready — paste the snippet to get started.
Install command
Provided
Config snippet
Provided
Copy snippet
Provided
Prerequisites
None
Platforms
1 listed
Difficulty
0/100
Adoption plan
Current risk score 16/100. Use staged verification before broader rollout.
Validate source and review signals before any execution.
Confirm source provenanceRequired
Source URL/provenance metadata is present.
Confirm metadata review state
Listing has review metadata.
Verify install payload
Install/config payload exists and can be inspected.
Confirm safety, privacy, and package integrity signals.
Review safety notesRequired
Safety notes are present.
Review privacy notesRequired
Privacy notes are present.
Verify package integrity metadata
No package verification/checksum metadata.
Adopt in controlled steps based on the selected plan.
Run in isolated sandbox firstRequired
Use a constrained sandbox and observe behavior across multiple tasks.
Roll out graduallyRequired
Roll out to a small cohort before wider usage.
Set monitoring and fallback
Define rollback path and monitor errors after adoption.
Evidence readiness
Required evidence gates are covered (5/6 signals complete).
Source repository/provenance is listed.
Required in this preset
Review metadata is present.
Required in this preset
Safety notes are present.
Required in this preset
Privacy notes are present.
Optional in this preset
Package integrity metadata is missing.
Optional in this preset
Install payload is available.
Required in this preset
Required evidence gates are covered for this preset.
Decision timeline
5/6 steps complete with no blocking gaps for this preset.
triage
Source/provenance metadata is available.
triage
Review metadata is available.
verify
Safety notes are available.
verify
Privacy notes are available.
verify
Package integrity metadata is missing.
rollout
Install payload is available.
No required blockers for this timeline preset.
Safety & privacy surface
2 safety and 2 privacy notes across 2 risk areas. Review closely: credentials & tokens.
#!/usr/bin/env bash
# Read the tool input from stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# Check if this is a markdown file
if [[ "$FILE_PATH" == *.md ]] || [[ "$FILE_PATH" == *.mdx ]] || [[ "$FILE_PATH" == *.markdown ]]; then
echo "🔗 Markdown Link Validation for: $(basename "$FILE_PATH")" >&2
# Initialize validation counters
ERRORS=0
WARNINGS=0
VALIDATIONS_PASSED=0
TOTAL_LINKS=0
EXTERNAL_LINKS=0
INTERNAL_LINKS=0
# Function to report validation results
report_validation() {
local level="$1"
local message="$2"
case "$level" in
"ERROR")
echo "❌ ERROR: $message" >&2
ERRORS=$((ERRORS + 1))
;;
"WARNING")
echo "⚠️ WARNING: $message" >&2
WARNINGS=$((WARNINGS + 1))
;;
"PASS")
echo "✅ PASS: $message" >&2
VALIDATIONS_PASSED=$((VALIDATIONS_PASSED + 1))
;;
"INFO")
echo "ℹ️ INFO: $message" >&2
;;
esac
}
# Check if file exists and is readable
if [ ! -f "$FILE_PATH" ]; then
report_validation "ERROR" "Markdown file not found: $FILE_PATH"
exit 1
fi
if [ ! -r "$FILE_PATH" ]; then
report_validation "ERROR" "Markdown file is not readable: $FILE_PATH"
exit 1
fi
# Get file information
FILE_NAME="$(basename "$FILE_PATH")"
FILE_DIR="$(dirname "$FILE_PATH")"
FILE_SIZE=$(wc -c < "$FILE_PATH" 2>/dev/null || echo "0")
LINE_COUNT=$(wc -l < "$FILE_PATH" 2>/dev/null || echo "0")
echo "📊 Markdown file: $FILE_NAME ($(( FILE_SIZE / 1024 ))KB, $LINE_COUNT lines)" >&2
# 1. Extract All Links from Markdown
echo "🔍 Extracting links from markdown..." >&2
# Create temporary files for link analysis
TEMP_LINKS="/tmp/markdown_links_$$"
TEMP_IMAGES="/tmp/markdown_images_$$"
TEMP_ANCHORS="/tmp/markdown_anchors_$$"
# Extract markdown links [text](url)
grep -oE '\[([^\]]+)\]\(([^)]+)\)' "$FILE_PATH" | sed 's/\[.*\](\(.*\))/\1/' > "$TEMP_LINKS" 2>/dev/null || true
# Extract image links 
grep -oE '!\[([^\]]*)\]\(([^)]+)\)' "$FILE_PATH" | sed 's/!\[.*\](\(.*\))/\1/' > "$TEMP_IMAGES" 2>/dev/null || true
# Extract reference-style links
grep -oE '\[([^\]]+)\]\[([^\]]+)\]' "$FILE_PATH" | sed 's/\[.*\]\[\(.*\)\]/\1/' >> "$TEMP_LINKS" 2>/dev/null || true
# Count total links
TOTAL_LINKS=$(cat "$TEMP_LINKS" "$TEMP_IMAGES" 2>/dev/null | wc -l || echo "0")
if [ "$TOTAL_LINKS" -eq 0 ]; then
echo " 📋 No links found in markdown file" >&2
report_validation "INFO" "No links to validate"
else
echo " 📊 Found $TOTAL_LINKS total links/images" >&2
fi
# 2. Validate External Links
echo "🌐 Validating external links..." >&2
# Try using markdown-link-check if available
if command -v npx &> /dev/null; then
echo " 🔍 Using markdown-link-check for comprehensive validation..." >&2
# Create a temporary config if none exists
CONFIG_FILE=".markdown-link-check.json"
TEMP_CONFIG=false
if [ ! -f "$CONFIG_FILE" ]; then
TEMP_CONFIG=true
CONFIG_FILE="/tmp/markdown_link_config_$$"
cat > "$CONFIG_FILE" << 'EOF'
{
"timeout": "30s",
"retryOn429": true,
"retryCount": 3,
"fallbackProtocols": ["http", "https"],
"ignorePatterns": [
{ "pattern": "^http://localhost" },
{ "pattern": "^https://localhost" },
{ "pattern": "^http://127.0.0.1" },
{ "pattern": "^#" }
]
}
EOF
fi
MLC_OUTPUT_FILE="/tmp/mlc_output_$$"
if timeout 60s npx markdown-link-check "$FILE_PATH" --config "$CONFIG_FILE" > "$MLC_OUTPUT_FILE" 2>&1; then
# Parse results
DEAD_LINKS=$(grep -c '✖' "$MLC_OUTPUT_FILE" 2>/dev/null || echo "0")
ALIVE_LINKS=$(grep -c '✓' "$MLC_OUTPUT_FILE" 2>/dev/null || echo "0")
if [ "$DEAD_LINKS" -eq 0 ]; then
report_validation "PASS" "All external links are valid ($ALIVE_LINKS checked)"
else
report_validation "ERROR" "Found $DEAD_LINKS dead external links"
echo " 📝 Dead links details:" >&2
grep '✖' "$MLC_OUTPUT_FILE" | head -5 | while read line; do
echo " $line" >&2
done
fi
else
# Fallback to basic URL validation
echo " ⚠️ markdown-link-check failed, using basic validation..." >&2
# Extract HTTP/HTTPS URLs
EXTERNAL_URLS=$(grep -oE 'https?://[^)]+' "$TEMP_LINKS" 2>/dev/null || true)
if [ -n "$EXTERNAL_URLS" ]; then
EXTERNAL_COUNT=$(echo "$EXTERNAL_URLS" | wc -l)
echo " 🌐 Found $EXTERNAL_COUNT external URLs to validate" >&2
# Basic URL validation using curl
if command -v curl &> /dev/null; then
EXTERNAL_ERRORS=0
echo "$EXTERNAL_URLS" | head -10 | while read -r url; do
if [ -n "$url" ]; then
if curl -s --head --max-time 10 "$url" >/dev/null 2>&1; then
echo " ✅ Valid: $url" >&2
else
echo " ❌ Invalid: $url" >&2
EXTERNAL_ERRORS=$((EXTERNAL_ERRORS + 1))
fi
fi
done
else
echo " ⚠️ curl not available for URL validation" >&2
fi
else
echo " 📋 No external URLs found" >&2
fi
fi
# Clean up temporary config if created
[ "$TEMP_CONFIG" = true ] && rm -f "$CONFIG_FILE"
rm -f "$MLC_OUTPUT_FILE"
else
echo " ⚠️ npx not available, using basic link validation" >&2
fi
# 3. Validate Internal Links
echo "📁 Validating internal links and references..." >&2
# Extract internal links (relative paths)
INTERNAL_URLS=$(cat "$TEMP_LINKS" "$TEMP_IMAGES" 2>/dev/null | grep -E '^\./|^\.\./' || true)
ABSOLUTE_PATHS=$(cat "$TEMP_LINKS" "$TEMP_IMAGES" 2>/dev/null | grep -E '^/' || true)
INTERNAL_ERRORS=0
# Check relative path links
if [ -n "$INTERNAL_URLS" ]; then
echo " 📂 Checking relative path links..." >&2
echo "$INTERNAL_URLS" | while read -r link; do
if [ -n "$link" ]; then
# Remove anchor if present
FILE_PART=$(echo "$link" | cut -d'#' -f1)
ANCHOR_PART=$(echo "$link" | cut -d'#' -f2)
if [ -n "$FILE_PART" ]; then
# Resolve relative path
RESOLVED_PATH="$(cd "$FILE_DIR" && realpath "$FILE_PART" 2>/dev/null || echo "$FILE_PART")"
if [ -f "$RESOLVED_PATH" ] || [ -d "$RESOLVED_PATH" ]; then
echo " ✅ Valid: $link" >&2
else
echo " ❌ Broken: $link (resolved to: $RESOLVED_PATH)" >&2
INTERNAL_ERRORS=$((INTERNAL_ERRORS + 1))
fi
fi
# Check anchor if present (simplified check)
if [ "$link" != "$FILE_PART" ] && [ -n "$ANCHOR_PART" ]; then
echo " ℹ️ Anchor found: #$ANCHOR_PART" >&2
fi
fi
done
fi
# Check absolute path links
if [ -n "$ABSOLUTE_PATHS" ]; then
echo " 📁 Checking absolute path links..." >&2
echo "$ABSOLUTE_PATHS" | while read -r link; do
if [ -n "$link" ]; then
FILE_PART=$(echo "$link" | cut -d'#' -f1)
if [ -f "$FILE_PART" ] || [ -d "$FILE_PART" ]; then
echo " ✅ Valid: $link" >&2
else
echo " ❌ Broken: $link" >&2
INTERNAL_ERRORS=$((INTERNAL_ERRORS + 1))
fi
fi
done
fi
if [ "$INTERNAL_ERRORS" -eq 0 ]; then
report_validation "PASS" "All internal links are valid"
else
report_validation "ERROR" "Found $INTERNAL_ERRORS broken internal links"
fi
# 4. Validate Image Links
echo "🖼️ Validating image links..." >&2
IMAGE_COUNT=$(cat "$TEMP_IMAGES" 2>/dev/null | wc -l || echo "0")
if [ "$IMAGE_COUNT" -gt 0 ]; then
echo " 📊 Found $IMAGE_COUNT image links" >&2
IMAGE_ERRORS=0
cat "$TEMP_IMAGES" | while read -r img_link; do
if [ -n "$img_link" ]; then
if [[ "$img_link" == http* ]]; then
echo " 🌐 External image: $img_link" >&2
else
# Local image file
if [[ "$img_link" == /* ]]; then
IMG_PATH="$img_link"
else
IMG_PATH="$FILE_DIR/$img_link"
fi
if [ -f "$IMG_PATH" ]; then
echo " ✅ Valid image: $img_link" >&2
else
echo " ❌ Missing image: $img_link" >&2
IMAGE_ERRORS=$((IMAGE_ERRORS + 1))
fi
fi
fi
done
if [ "$IMAGE_ERRORS" -eq 0 ]; then
report_validation "PASS" "All local images found"
else
report_validation "ERROR" "$IMAGE_ERRORS local images missing"
fi
else
echo " 📋 No image links found" >&2
fi
# 5. Validate Internal Anchors
echo "⚓ Validating document anchors..." >&2
# Extract anchor-only links (starting with #)
ANCHOR_LINKS=$(cat "$TEMP_LINKS" | grep '^#' 2>/dev/null || true)
if [ -n "$ANCHOR_LINKS" ]; then
echo " 🔗 Found anchor links to validate" >&2
# Extract headers from markdown to validate anchors
HEADERS_FILE="/tmp/markdown_headers_$$"
grep -E '^#{1,6} ' "$FILE_PATH" | sed 's/^#* *//' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ /-/g' > "$HEADERS_FILE" 2>/dev/null || true
ANCHOR_ERRORS=0
echo "$ANCHOR_LINKS" | while read -r anchor; do
if [ -n "$anchor" ]; then
CLEAN_ANCHOR=$(echo "$anchor" | sed 's/^#//' | tr '[:upper:]' '[:lower:]')
if grep -q "^$CLEAN_ANCHOR$" "$HEADERS_FILE" 2>/dev/null; then
echo " ✅ Valid anchor: $anchor" >&2
else
echo " ❌ Invalid anchor: $anchor" >&2
ANCHOR_ERRORS=$((ANCHOR_ERRORS + 1))
fi
fi
done
rm -f "$HEADERS_FILE"
if [ "$ANCHOR_ERRORS" -eq 0 ]; then
report_validation "PASS" "All document anchors valid"
else
report_validation "ERROR" "$ANCHOR_ERRORS invalid document anchors"
fi
else
echo " 📋 No document anchors found" >&2
fi
# 6. Markdown Quality Checks
echo "📝 Markdown quality and accessibility checks..." >&2
# Check for alt text in images
IMAGES_WITHOUT_ALT=$(grep -c '!\[\](' "$FILE_PATH" 2>/dev/null || echo "0")
if [ "$IMAGES_WITHOUT_ALT" -gt 0 ]; then
report_validation "WARNING" "$IMAGES_WITHOUT_ALT images missing alt text for accessibility"
else
if [ "$IMAGE_COUNT" -gt 0 ]; then
report_validation "PASS" "All images have alt text"
fi
fi
# Check for bare URLs (not wrapped in markdown links)
BARE_URLS=$(grep -oE 'https?://[^\s\)\]]+' "$FILE_PATH" | grep -v '](http' | head -5 | wc -l || echo "0")
if [ "$BARE_URLS" -gt 0 ]; then
report_validation "WARNING" "Found $BARE_URLS bare URLs - consider wrapping in markdown links"
fi
# 7. Generate Validation Summary
echo "" >&2
echo "📋 Markdown Link Validation Summary:" >&2
echo "===================================" >&2
echo " 📄 File: $FILE_NAME" >&2
echo " 📏 Size: $(( FILE_SIZE / 1024 ))KB, $LINE_COUNT lines" >&2
echo " 🔗 Total links: $TOTAL_LINKS" >&2
echo " 🖼️ Images: $IMAGE_COUNT" >&2
echo " ✅ Validations passed: $VALIDATIONS_PASSED" >&2
echo " ⚠️ Warnings: $WARNINGS" >&2
echo " ❌ Errors: $ERRORS" >&2
if [ "$ERRORS" -eq 0 ]; then
if [ "$WARNINGS" -eq 0 ]; then
echo " 🎉 Status: EXCELLENT - All links are valid and accessible" >&2
else
echo " ✅ Status: GOOD - Links are valid with minor accessibility recommendations" >&2
fi
else
echo " ❌ Status: ERRORS - Found broken links that must be fixed" >&2
fi
echo "" >&2
echo "💡 Markdown Link Best Practices:" >&2
echo " • Use descriptive link text instead of 'click here'" >&2
echo " • Add alt text to all images for accessibility" >&2
echo " • Use relative paths for internal documentation" >&2
echo " • Validate external links regularly" >&2
echo " • Keep anchor links synchronized with headers" >&2
echo " • Consider using reference-style links for readability" >&2
# Clean up temporary files
rm -f "$TEMP_LINKS" "$TEMP_IMAGES" "$TEMP_ANCHORS"
# Exit with error if there are critical link issues
if [ "$ERRORS" -gt 0 ]; then
echo "⚠️ Markdown link validation completed with errors" >&2
exit 1
fi
else
# Not a markdown file
exit 0
fi
exit 0{
"hooks": {
"postToolUse": {
"script": "./.claude/hooks/markdown-link-checker.sh",
"matchers": [
"write",
"edit"
]
}
}
}.claude/settings.local.json~/.claude/settings.json.claude/settings.json{
"hooks": {
"postToolUse": {
"script": "./.claude/hooks/markdown-link-checker.sh",
"matchers": ["write", "edit"]
}
}
}
#!/usr/bin/env bash
# Read the tool input from stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# Check if this is a markdown file
if [[ "$FILE_PATH" == *.md ]] || [[ "$FILE_PATH" == *.mdx ]] || [[ "$FILE_PATH" == *.markdown ]]; then
echo "🔗 Markdown Link Validation for: $(basename "$FILE_PATH")" >&2
# Initialize validation counters
ERRORS=0
WARNINGS=0
VALIDATIONS_PASSED=0
TOTAL_LINKS=0
EXTERNAL_LINKS=0
INTERNAL_LINKS=0
# Function to report validation results
report_validation() {
local level="$1"
local message="$2"
case "$level" in
"ERROR")
echo "❌ ERROR: $message" >&2
ERRORS=$((ERRORS + 1))
;;
"WARNING")
echo "⚠️ WARNING: $message" >&2
WARNINGS=$((WARNINGS + 1))
;;
"PASS")
echo "✅ PASS: $message" >&2
VALIDATIONS_PASSED=$((VALIDATIONS_PASSED + 1))
;;
"INFO")
echo "ℹ️ INFO: $message" >&2
;;
esac
}
# Check if file exists and is readable
if [ ! -f "$FILE_PATH" ]; then
report_validation "ERROR" "Markdown file not found: $FILE_PATH"
exit 1
fi
if [ ! -r "$FILE_PATH" ]; then
report_validation "ERROR" "Markdown file is not readable: $FILE_PATH"
exit 1
fi
# Get file information
FILE_NAME="$(basename "$FILE_PATH")"
FILE_DIR="$(dirname "$FILE_PATH")"
FILE_SIZE=$(wc -c < "$FILE_PATH" 2>/dev/null || echo "0")
LINE_COUNT=$(wc -l < "$FILE_PATH" 2>/dev/null || echo "0")
echo "📊 Markdown file: $FILE_NAME ($(( FILE_SIZE / 1024 ))KB, $LINE_COUNT lines)" >&2
# 1. Extract All Links from Markdown
echo "🔍 Extracting links from markdown..." >&2
# Create temporary files for link analysis
TEMP_LINKS="/tmp/markdown_links_$$"
TEMP_IMAGES="/tmp/markdown_images_$$"
TEMP_ANCHORS="/tmp/markdown_anchors_$$"
# Extract markdown links [text](url)
grep -oE '\[([^\]]+)\]\(([^)]+)\)' "$FILE_PATH" | sed 's/\[.*\](\(.*\))/\1/' > "$TEMP_LINKS" 2>/dev/null || true
# Extract image links 
grep -oE '!\[([^\]]*)\]\(([^)]+)\)' "$FILE_PATH" | sed 's/!\[.*\](\(.*\))/\1/' > "$TEMP_IMAGES" 2>/dev/null || true
# Extract reference-style links
grep -oE '\[([^\]]+)\]\[([^\]]+)\]' "$FILE_PATH" | sed 's/\[.*\]\[\(.*\)\]/\1/' >> "$TEMP_LINKS" 2>/dev/null || true
# Count total links
TOTAL_LINKS=$(cat "$TEMP_LINKS" "$TEMP_IMAGES" 2>/dev/null | wc -l || echo "0")
if [ "$TOTAL_LINKS" -eq 0 ]; then
echo " 📋 No links found in markdown file" >&2
report_validation "INFO" "No links to validate"
else
echo " 📊 Found $TOTAL_LINKS total links/images" >&2
fi
# 2. Validate External Links
echo "🌐 Validating external links..." >&2
# Try using markdown-link-check if available
if command -v npx &> /dev/null; then
echo " 🔍 Using markdown-link-check for comprehensive validation..." >&2
# Create a temporary config if none exists
CONFIG_FILE=".markdown-link-check.json"
TEMP_CONFIG=false
if [ ! -f "$CONFIG_FILE" ]; then
TEMP_CONFIG=true
CONFIG_FILE="/tmp/markdown_link_config_$$"
cat > "$CONFIG_FILE" << 'EOF'
{
"timeout": "30s",
"retryOn429": true,
"retryCount": 3,
"fallbackProtocols": ["http", "https"],
"ignorePatterns": [
{ "pattern": "^http://localhost" },
{ "pattern": "^https://localhost" },
{ "pattern": "^http://127.0.0.1" },
{ "pattern": "^#" }
]
}
EOF
fi
MLC_OUTPUT_FILE="/tmp/mlc_output_$$"
if timeout 60s npx markdown-link-check "$FILE_PATH" --config "$CONFIG_FILE" > "$MLC_OUTPUT_FILE" 2>&1; then
# Parse results
DEAD_LINKS=$(grep -c '✖' "$MLC_OUTPUT_FILE" 2>/dev/null || echo "0")
ALIVE_LINKS=$(grep -c '✓' "$MLC_OUTPUT_FILE" 2>/dev/null || echo "0")
if [ "$DEAD_LINKS" -eq 0 ]; then
report_validation "PASS" "All external links are valid ($ALIVE_LINKS checked)"
else
report_validation "ERROR" "Found $DEAD_LINKS dead external links"
echo " 📝 Dead links details:" >&2
grep '✖' "$MLC_OUTPUT_FILE" | head -5 | while read line; do
echo " $line" >&2
done
fi
else
# Fallback to basic URL validation
echo " ⚠️ markdown-link-check failed, using basic validation..." >&2
# Extract HTTP/HTTPS URLs
EXTERNAL_URLS=$(grep -oE 'https?://[^)]+' "$TEMP_LINKS" 2>/dev/null || true)
if [ -n "$EXTERNAL_URLS" ]; then
EXTERNAL_COUNT=$(echo "$EXTERNAL_URLS" | wc -l)
echo " 🌐 Found $EXTERNAL_COUNT external URLs to validate" >&2
# Basic URL validation using curl
if command -v curl &> /dev/null; then
EXTERNAL_ERRORS=0
echo "$EXTERNAL_URLS" | head -10 | while read -r url; do
if [ -n "$url" ]; then
if curl -s --head --max-time 10 "$url" >/dev/null 2>&1; then
echo " ✅ Valid: $url" >&2
else
echo " ❌ Invalid: $url" >&2
EXTERNAL_ERRORS=$((EXTERNAL_ERRORS + 1))
fi
fi
done
else
echo " ⚠️ curl not available for URL validation" >&2
fi
else
echo " 📋 No external URLs found" >&2
fi
fi
# Clean up temporary config if created
[ "$TEMP_CONFIG" = true ] && rm -f "$CONFIG_FILE"
rm -f "$MLC_OUTPUT_FILE"
else
echo " ⚠️ npx not available, using basic link validation" >&2
fi
# 3. Validate Internal Links
echo "📁 Validating internal links and references..." >&2
# Extract internal links (relative paths)
INTERNAL_URLS=$(cat "$TEMP_LINKS" "$TEMP_IMAGES" 2>/dev/null | grep -E '^\./|^\.\./' || true)
ABSOLUTE_PATHS=$(cat "$TEMP_LINKS" "$TEMP_IMAGES" 2>/dev/null | grep -E '^/' || true)
INTERNAL_ERRORS=0
# Check relative path links
if [ -n "$INTERNAL_URLS" ]; then
echo " 📂 Checking relative path links..." >&2
echo "$INTERNAL_URLS" | while read -r link; do
if [ -n "$link" ]; then
# Remove anchor if present
FILE_PART=$(echo "$link" | cut -d'#' -f1)
ANCHOR_PART=$(echo "$link" | cut -d'#' -f2)
if [ -n "$FILE_PART" ]; then
# Resolve relative path
RESOLVED_PATH="$(cd "$FILE_DIR" && realpath "$FILE_PART" 2>/dev/null || echo "$FILE_PART")"
if [ -f "$RESOLVED_PATH" ] || [ -d "$RESOLVED_PATH" ]; then
echo " ✅ Valid: $link" >&2
else
echo " ❌ Broken: $link (resolved to: $RESOLVED_PATH)" >&2
INTERNAL_ERRORS=$((INTERNAL_ERRORS + 1))
fi
fi
# Check anchor if present (simplified check)
if [ "$link" != "$FILE_PART" ] && [ -n "$ANCHOR_PART" ]; then
echo " ℹ️ Anchor found: #$ANCHOR_PART" >&2
fi
fi
done
fi
# Check absolute path links
if [ -n "$ABSOLUTE_PATHS" ]; then
echo " 📁 Checking absolute path links..." >&2
echo "$ABSOLUTE_PATHS" | while read -r link; do
if [ -n "$link" ]; then
FILE_PART=$(echo "$link" | cut -d'#' -f1)
if [ -f "$FILE_PART" ] || [ -d "$FILE_PART" ]; then
echo " ✅ Valid: $link" >&2
else
echo " ❌ Broken: $link" >&2
INTERNAL_ERRORS=$((INTERNAL_ERRORS + 1))
fi
fi
done
fi
if [ "$INTERNAL_ERRORS" -eq 0 ]; then
report_validation "PASS" "All internal links are valid"
else
report_validation "ERROR" "Found $INTERNAL_ERRORS broken internal links"
fi
# 4. Validate Image Links
echo "🖼️ Validating image links..." >&2
IMAGE_COUNT=$(cat "$TEMP_IMAGES" 2>/dev/null | wc -l || echo "0")
if [ "$IMAGE_COUNT" -gt 0 ]; then
echo " 📊 Found $IMAGE_COUNT image links" >&2
IMAGE_ERRORS=0
cat "$TEMP_IMAGES" | while read -r img_link; do
if [ -n "$img_link" ]; then
if [[ "$img_link" == http* ]]; then
echo " 🌐 External image: $img_link" >&2
else
# Local image file
if [[ "$img_link" == /* ]]; then
IMG_PATH="$img_link"
else
IMG_PATH="$FILE_DIR/$img_link"
fi
if [ -f "$IMG_PATH" ]; then
echo " ✅ Valid image: $img_link" >&2
else
echo " ❌ Missing image: $img_link" >&2
IMAGE_ERRORS=$((IMAGE_ERRORS + 1))
fi
fi
fi
done
if [ "$IMAGE_ERRORS" -eq 0 ]; then
report_validation "PASS" "All local images found"
else
report_validation "ERROR" "$IMAGE_ERRORS local images missing"
fi
else
echo " 📋 No image links found" >&2
fi
# 5. Validate Internal Anchors
echo "⚓ Validating document anchors..." >&2
# Extract anchor-only links (starting with #)
ANCHOR_LINKS=$(cat "$TEMP_LINKS" | grep '^#' 2>/dev/null || true)
if [ -n "$ANCHOR_LINKS" ]; then
echo " 🔗 Found anchor links to validate" >&2
# Extract headers from markdown to validate anchors
HEADERS_FILE="/tmp/markdown_headers_$$"
grep -E '^#{1,6} ' "$FILE_PATH" | sed 's/^#* *//' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ /-/g' > "$HEADERS_FILE" 2>/dev/null || true
ANCHOR_ERRORS=0
echo "$ANCHOR_LINKS" | while read -r anchor; do
if [ -n "$anchor" ]; then
CLEAN_ANCHOR=$(echo "$anchor" | sed 's/^#//' | tr '[:upper:]' '[:lower:]')
if grep -q "^$CLEAN_ANCHOR$" "$HEADERS_FILE" 2>/dev/null; then
echo " ✅ Valid anchor: $anchor" >&2
else
echo " ❌ Invalid anchor: $anchor" >&2
ANCHOR_ERRORS=$((ANCHOR_ERRORS + 1))
fi
fi
done
rm -f "$HEADERS_FILE"
if [ "$ANCHOR_ERRORS" -eq 0 ]; then
report_validation "PASS" "All document anchors valid"
else
report_validation "ERROR" "$ANCHOR_ERRORS invalid document anchors"
fi
else
echo " 📋 No document anchors found" >&2
fi
# 6. Markdown Quality Checks
echo "📝 Markdown quality and accessibility checks..." >&2
# Check for alt text in images
IMAGES_WITHOUT_ALT=$(grep -c '!\[\](' "$FILE_PATH" 2>/dev/null || echo "0")
if [ "$IMAGES_WITHOUT_ALT" -gt 0 ]; then
report_validation "WARNING" "$IMAGES_WITHOUT_ALT images missing alt text for accessibility"
else
if [ "$IMAGE_COUNT" -gt 0 ]; then
report_validation "PASS" "All images have alt text"
fi
fi
# Check for bare URLs (not wrapped in markdown links)
BARE_URLS=$(grep -oE 'https?://[^\s\)\]]+' "$FILE_PATH" | grep -v '](http' | head -5 | wc -l || echo "0")
if [ "$BARE_URLS" -gt 0 ]; then
report_validation "WARNING" "Found $BARE_URLS bare URLs - consider wrapping in markdown links"
fi
# 7. Generate Validation Summary
echo "" >&2
echo "📋 Markdown Link Validation Summary:" >&2
echo "===================================" >&2
echo " 📄 File: $FILE_NAME" >&2
echo " 📏 Size: $(( FILE_SIZE / 1024 ))KB, $LINE_COUNT lines" >&2
echo " 🔗 Total links: $TOTAL_LINKS" >&2
echo " 🖼️ Images: $IMAGE_COUNT" >&2
echo " ✅ Validations passed: $VALIDATIONS_PASSED" >&2
echo " ⚠️ Warnings: $WARNINGS" >&2
echo " ❌ Errors: $ERRORS" >&2
if [ "$ERRORS" -eq 0 ]; then
if [ "$WARNINGS" -eq 0 ]; then
echo " 🎉 Status: EXCELLENT - All links are valid and accessible" >&2
else
echo " ✅ Status: GOOD - Links are valid with minor accessibility recommendations" >&2
fi
else
echo " ❌ Status: ERRORS - Found broken links that must be fixed" >&2
fi
echo "" >&2
echo "💡 Markdown Link Best Practices:" >&2
echo " • Use descriptive link text instead of 'click here'" >&2
echo " • Add alt text to all images for accessibility" >&2
echo " • Use relative paths for internal documentation" >&2
echo " • Validate external links regularly" >&2
echo " • Keep anchor links synchronized with headers" >&2
echo " • Consider using reference-style links for readability" >&2
# Clean up temporary files
rm -f "$TEMP_LINKS" "$TEMP_IMAGES" "$TEMP_ANCHORS"
# Exit with error if there are critical link issues
if [ "$ERRORS" -gt 0 ]; then
echo "⚠️ Markdown link validation completed with errors" >&2
exit 1
fi
else
# Not a markdown file
exit 0
fi
exit 0
Complete hook script that performs markdown link validation
#!/usr/bin/env bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
if [[ "$FILE_PATH" == *.md ]] || [[ "$FILE_PATH" == *.mdx ]]; then
echo "🔗 Markdown Link Validation for: $(basename "$FILE_PATH")" >&2
if command -v npx &> /dev/null; then
if timeout 60s npx markdown-link-check "$FILE_PATH" --config .markdown-link-check.json 2>/dev/null; then
echo "✅ All links valid" >&2
else
echo "❌ Broken links detected" >&2
exit 1
fi
fi
fi
exit 0
Complete hook configuration for .claude/settings.json to enable markdown link validation
{
"hooks": {
"postToolUse": {
"script": "./.claude/hooks/markdown-link-checker.sh",
"matchers": ["write", "edit"]
}
}
}
Example .markdown-link-check.json configuration file for customizing link validation settings
{
"timeout": "30s",
"retryOn429": true,
"retryCount": 3,
"fallbackProtocols": ["http", "https"],
"ignorePatterns": [
{ "pattern": "^http://localhost" },
{ "pattern": "^https://localhost" },
{ "pattern": "^http://127.0.0.1" },
{ "pattern": "^#" }
]
}
Enhanced hook script for internal link validation with path resolution
#!/usr/bin/env bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
FILE_DIR="$(dirname "$FILE_PATH")"
if [[ "$FILE_PATH" == *.md ]]; then
INTERNAL_LINKS=$(grep -oE '\\([^)]+\\)' "$FILE_PATH" | sed 's/[()]//g' | grep -E '^\./|^\.\./')
INTERNAL_ERRORS=0
echo "$INTERNAL_LINKS" | while read -r link; do
if [ -n "$link" ]; then
FILE_PART=$(echo "$link" | cut -d'#' -f1)
RESOLVED_PATH="$(cd "$FILE_DIR" && realpath "$FILE_PART" 2>/dev/null || echo "$FILE_PART")"
if [ ! -f "$RESOLVED_PATH" ] && [ ! -d "$RESOLVED_PATH" ]; then
echo "❌ Broken: $link" >&2
INTERNAL_ERRORS=$((INTERNAL_ERRORS + 1))
fi
fi
done
fi
exit 0
Enhanced hook script for anchor link validation with heading ID matching
#!/usr/bin/env bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""')
if [[ "$FILE_PATH" == *.md ]]; then
HEADERS=$(grep -E '^#{1,6} ' "$FILE_PATH" | sed 's/^#* *//' | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ /-/g')
ANCHOR_LINKS=$(grep -oE '\\(#[^)]+\\)' "$FILE_PATH" | sed 's/[()]//g' | sed 's/^#//')
ANCHOR_ERRORS=0
echo "$ANCHOR_LINKS" | while read -r anchor; do
if [ -n "$anchor" ]; then
CLEAN_ANCHOR=$(echo "$anchor" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9 -]//g' | sed 's/ /-/g')
if ! echo "$HEADERS" | grep -q "^$CLEAN_ANCHOR$"; then
echo "❌ Invalid anchor: #$anchor" >&2
ANCHOR_ERRORS=$((ANCHOR_ERRORS + 1))
fi
fi
done
fi
exit 0
Increase timeout in markdown-link-check config from 30s to 60s. Reduce retryCount from 3 to 1 for faster failures. Use --quiet flag or limit external link checking to critical links only. Verify timeout settings. Test with various file sizes.
Add localhost patterns to ignorePatterns in .markdown-link-check.json config. Hook creates temp config with common exclusions but customize project config for development server URLs. Verify ignore patterns. Test with various URL patterns.
Heading ID generation varies by markdown processor. Update anchor cleaning logic in hook to match your tool's slug generation (GitHub uses lowercase with dashes, Jekyll may differ). Verify heading ID format. Test with various markdown processors.
Check path resolution relative to markdown file location. Hook resolves paths from file directory, not repo root. Use absolute paths from repo root with leading slash for consistency. Verify path resolution. Test with various path formats.
Enable retryOn429 in config and increase timeout. Add rate-limited domains to ignorePatterns temporarily. Consider running full external validation in CI only, not on every file edit. Verify retry settings. Test with various external URLs.
realpath resolves symlinks by default. Check if symlink target exists: readlink -f "$link" or use -s flag with realpath. Verify symlink handling. Test with various symlink configurations.
Hook extracts reference-style links with pattern: grep -oE '\[([^\]]+)\]\[([^\]]+)\]'. Verify link format matches expected pattern. Check for link definition section. Test with various reference-style link formats.
Hook checks for empty alt text: grep -c '!\[\]('. Some images may legitimately have empty alt text (decorative images). Adjust validation logic or add exceptions. Verify alt text patterns. Test with various image formats.
Markdown Link Checker - Hooks side by side with 2 alternatives on trust, install, platform support, and disclosed safety notes — all from reviewed registry metadata.
| Field | Validates all links in markdown files to detect broken links and references. Open dossier | Automatically generates or updates project documentation when session ends. Open dossier | Automated documentation coverage analysis with missing docstring detection, API documentation validation, and completeness scoring. This PostToolUse hook automatically checks documentation coverage when code files are modified, providing real-time documentation quality validation during development. Open dossier |
|---|---|---|---|
| Next steps | |||
| Trust | |||
| Review status | ReviewedMaintainer reviewed | ReviewedMaintainer reviewed | ReviewedMaintainer reviewed |
| Package trust | Package not verified | Package not verified | Package not verified |
| Source provenance | Source-backed | Source-backed | Source-backed |
| Submitter | — | — | — |
| Install risk | Review first | Review first | Review first |
| Notes | Safety ✓ Privacy ✓ | Safety ✓ Privacy ✓ | Safety ✓ Privacy ✓ |
| Brand | — | — | — |
| Category | hooks | hooks | hooks |
| Source | source-backed | source-backed | source-backed |
| Author | JSONbored | JSONbored | JSONbored |
| Added | 2025-09-19 | 2025-09-19 | 2025-10-19 |
| Platforms | Claude Code | Claude Code | Claude Code |
| Source repo | — | — | — |
| Safety notes | ✓Runs as part of an automated hook or local workflow; test on a disposable branch before enabling it on write or blocking paths. Review generated reports, file edits, benchmark output, or shell commands before treating hook output as a merge or release gate. | ✓Runs automatically on its configured Claude Code hook event and executes shell logic that can read, modify, or delete files in your project (and may run builds, installs, or network calls); review the script and scope it to expected paths before enabling. | ✓Runs automatically on its configured Claude Code hook event and executes shell logic that can read, modify, or delete files in your project (and may run builds, installs, or network calls); review the script and scope it to expected paths before enabling. |
| Privacy notes | ✓Hook inputs and reports can include repository paths, markdown content, benchmark results, lint findings, environment details, or CI logs. Redact private URLs, tokens, customer data, and proprietary file contents before publishing hook output. | ✓Receives Claude Code hook input (session metadata, file paths, and tool output) and reads local project files; review what the script logs or forwards to external services and keep credentials out of its output. | ✓Receives Claude Code hook input (session metadata, file paths, and tool output) and reads local project files; review what the script logs or forwards to external services and keep credentials out of its output. |
| Prerequisites | — none listed | — none listed | — none listed |
| Install | | | |
| Config | | | |
| Citations | |||
| Claim | Unclaimed | Unclaimed | Unclaimed |
Source-backed guides for putting this to work.
Loading live community signals…
A short, calm digest of reviewed Claude resources. Unsubscribe any time.