All skills
simota avatar

/latch

@16a3f28
by shingo imotasimota/agent-skills85 stars
15

Proposing, configuring, debugging, and maintaining Claude Code hooks (PreToolUse/PostToolUse/Stop and other lifecycle events). Use for workflow automation or quality gates via hooks.

Use this Skill: https://skilld.dev/gh/simota/agent-skills/latch

This session only. Nothing lands on disk.

referencesecurity-guard-hook.md

≈2.2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Security Guard Hook

Reference for Latch's security recipe. PreToolUse hooks for blocking dangerous operations, secret detection, sensitive file protection, and MCP tool ACL.

Defense-in-depth alongside _common/BOUNDARIES.md. Hooks enforce at runtime; agent boundaries inform intent.


1. PreToolUse Decision Model

PreToolUse hook stdin:

{
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash" | "Write" | "Edit" | "mcp__*",
  "tool_input": { ... },
  "session_id": "uuid",
  "cwd": "/path"
}

Output (stdout JSON):

{
  "permissionDecision": "allow" | "deny" | "ask" | "defer",
  "reason": "human-readable rationale",
  "updatedInput": { ... }   // optional, only with "allow"
}

Or exit code 2 to immediately block (simpler for deny-only logic).


2. Dangerous Bash Patterns

#!/bin/bash
# ~/.claude/scripts/security-bash-guard.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r .tool_name)

[ "$TOOL" != "Bash" ] && { echo '{"permissionDecision":"allow"}'; exit 0; }

CMD=$(echo "$INPUT" | jq -r .tool_input.command)

# Patterns to deny
DENY_PATTERNS=(
  'rm -rf /'
  'rm -rf ~'
  'rm -rf \*'
  'chmod -R 777'
  'mkfs\.'
  'dd if=.+of=/dev/'
  ':\(\)\{ :\|:& \};:'  # fork bomb
  'curl .+ \| (sudo )?bash'
  'wget .+ \| (sudo )?bash'
  '> /dev/sda'
)

for PATTERN in "${DENY_PATTERNS[@]}"; do
  if echo "$CMD" | grep -qE "$PATTERN"; then
    jq -n --arg reason "Blocked: matches $PATTERN" \
      '{permissionDecision:"deny",reason:$reason}'
    exit 0
  fi
done

# git push --force to main/master
if echo "$CMD" | grep -qE 'git push.+--force.+(main|master)\b' || \
   echo "$CMD" | grep -qE 'git push.+--force\b.*\b(main|master)\s*$'; then
  jq -n '{permissionDecision:"deny",reason:"Blocked: force-push to main/master"}'
  exit 0
fi

echo '{"permissionDecision":"allow"}'

3. Sensitive File Protection (Write/Edit)

#!/bin/bash
# ~/.claude/scripts/security-file-guard.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r .tool_name)

case "$TOOL" in
  Write|Edit) ;;
  *) echo '{"permissionDecision":"allow"}'; exit 0 ;;
esac

PATH_VAL=$(echo "$INPUT" | jq -r .tool_input.file_path)

# Sensitive file patterns
SENSITIVE_PATTERNS=(
  '\.env$'
  '\.env\.'
  '/id_rsa$'
  '/id_ed25519$'
  '\.pem$'
  '\.p12$'
  '\.key$'
  '/secrets\.json$'
  '/credentials(\.json)?$'
  '/\.npmrc$'   # may contain auth tokens
  '/\.aws/credentials$'
  '/\.gcloud/credentials\.db$'
)

for PATTERN in "${SENSITIVE_PATTERNS[@]}"; do
  if echo "$PATH_VAL" | grep -qE "$PATTERN"; then
    jq -n --arg p "$PATH_VAL" \
      '{permissionDecision:"ask",reason:"Sensitive file: \($p) requires confirmation"}'
    exit 0
  fi
done

echo '{"permissionDecision":"allow"}'

For higher security: use "deny" instead of "ask".


4. Secret Regex in Tool Input

Detect secrets being written/passed:

#!/bin/bash
# ~/.claude/scripts/security-secret-guard.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r .tool_name)

case "$TOOL" in
  Write|Edit) CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // .tool_input.new_string') ;;
  Bash) CONTENT=$(echo "$INPUT" | jq -r .tool_input.command) ;;
  *) echo '{"permissionDecision":"allow"}'; exit 0 ;;
esac

# Common secret patterns
SECRET_PATTERNS=(
  'AKIA[0-9A-Z]{16}'                               # AWS access key
  'aws_secret_access_key.*[A-Za-z0-9/+=]{40}'      # AWS secret
  'sk-[A-Za-z0-9]{40,}'                            # OpenAI / Stripe / generic
  'sk-ant-api[0-9]{2}-[A-Za-z0-9_-]{90,}'          # Anthropic API key
  'ghp_[A-Za-z0-9]{36}'                            # GitHub PAT
  'github_pat_[A-Za-z0-9_]{82}'                    # GitHub fine-grained PAT
  'xox[baprs]-[A-Za-z0-9-]{10,}'                   # Slack token
  'eyJ[A-Za-z0-9_-]{20,}\.eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}'  # JWT
  '-----BEGIN [A-Z ]+PRIVATE KEY-----'             # PEM private key
)

for PATTERN in "${SECRET_PATTERNS[@]}"; do
  if echo "$CONTENT" | grep -qE "$PATTERN"; then
    jq -n --arg p "$(echo "$PATTERN" | head -c 30)..." \
      '{permissionDecision:"deny",reason:"Secret detected: \($p)"}'
    exit 0
  fi
done

echo '{"permissionDecision":"allow"}'

Auto-redact (for logging hooks, not for blocking)

REDACTED=$(echo "$CONTENT" | sed -E 's/(sk-[A-Za-z0-9]{40,})/[REDACTED-OPENAI]/g')
jq -n --arg c "$REDACTED" '{permissionDecision:"allow",updatedInput:{content:$c}}'

5. MCP Tool ACL

#!/bin/bash
# ~/.claude/scripts/security-mcp-acl.sh
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r .tool_name)

# Only filter MCP tools
[[ "$TOOL" != mcp__* ]] && { echo '{"permissionDecision":"allow"}'; exit 0; }

# Read denylist from env (comma-separated)
DENYLIST="${LATCH_BLOCKED_MCP_TOOLS:-}"

IFS=',' read -ra DENIED <<< "$DENYLIST"
for D in "${DENIED[@]}"; do
  if [ "$TOOL" = "$D" ] || [[ "$TOOL" == "$D"* ]]; then
    jq -n --arg t "$TOOL" \
      '{permissionDecision:"deny",reason:"MCP tool \($t) is on denylist"}'
    exit 0
  fi
done

echo '{"permissionDecision":"allow"}'

Usage:

export LATCH_BLOCKED_MCP_TOOLS="mcp__shell__exec,mcp__notion__delete_page"

6. CI Auto-Deny Escalation

In CI environments, "ask" cannot be answered. Escalate all asks to deny:

if [ "$CI" = "true" ] || [ -n "$GITHUB_ACTIONS" ]; then
  # In CI, asks become denies
  ORIGINAL_DECISION=$(echo "$DECISION_JSON" | jq -r .permissionDecision)
  if [ "$ORIGINAL_DECISION" = "ask" ]; then
    echo "$DECISION_JSON" | jq '.permissionDecision = "deny" | .reason = "Auto-denied (ask in CI): \(.reason)"'
    exit 0
  fi
fi

7. Compositions

Single hook composing all guards:

#!/bin/bash
# ~/.claude/scripts/security-all.sh
INPUT=$(cat)

# Run guards in order; first deny wins
for GUARD in bash file secret mcp; do
  RESULT=$(echo "$INPUT" | ~/.claude/scripts/security-${GUARD}-guard.sh)
  DECISION=$(echo "$RESULT" | jq -r .permissionDecision)
  if [ "$DECISION" = "deny" ] || [ "$DECISION" = "ask" ]; then
    echo "$RESULT"
    exit 0
  fi
done

echo '{"permissionDecision":"allow"}'

Settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "~/.claude/scripts/security-all.sh", "timeout": 2000 }
        ]
      }
    ]
  }
}

8. Audit Logging (PostToolUse, complementary)

Log all tool uses for security audit:

#!/bin/bash
# ~/.claude/scripts/security-audit-log.sh
INPUT=$(cat)
LOG=~/.claude/audit/$(date +%Y-%m).jsonl
mkdir -p "$(dirname "$LOG")"
echo "$INPUT" | jq -c '. + {logged_at: now}' >> "$LOG"
exit 0
{
  "PostToolUse": [
    {
      "matcher": "",
      "hooks": [{ "type": "command", "command": "~/.claude/scripts/security-audit-log.sh", "async": true }]
    }
  ]
}

9. Common Pitfalls

Pitfall Avoidance
Regex too broad → blocks legitimate ops Test against false-positive corpus
Regex too narrow → misses variants Test with obfuscated examples
Hook crashes → tool silently allowed Always exit 0 with explicit decision; use set -e carefully
Slow regex → tool feels laggy Keep <100ms per check
Secret patterns hardcoded → outdated Update list periodically; consider gitleaks rule sync
jq missing → hook fails Document install requirement
Asking for confirmation in CI → hangs Detect $CI; auto-deny
Allowing git push --force to PR branches Allowlist non-main branches
Blocking own .env edit when bootstrapping Provide BYPASS=1 env var with explicit user opt-in
Logging secrets to audit log Redact secrets before logging

10. Decision Walkthrough Template

Threat surface:
  □ Dangerous Bash (rm -rf /, fork bomb, curl|bash)
  □ Sensitive file write (.env, .pem, secrets)
  □ Secret leakage in input (API keys, JWTs, PEM)
  □ MCP tool ACL (denylist via env)

Decision behavior per match:
  □ deny (block + log)
  □ ask (interactive confirm)
  □ allow + redact (updatedInput)

CI behavior:
  □ Escalate ask → deny
  □ Allow opt-out via BYPASS=1 env

Performance budget: <100ms per hook

Audit:
  □ PostToolUse audit log (JSONL with redaction)
  □ Log path: ~/.claude/audit/YYYY-MM.jsonl

Setup:
  □ chmod +x scripts
  □ Test with malicious input examples
  □ Document BYPASS env var usage
  □ Session restart

11. References

  • Claude Code hooks documentation
  • gitleaks (secret pattern reference)
  • OWASP Cheat Sheet: Logging
  • AWS / GitHub / OpenAI / Slack secret format docs
  • Knight Capital 2012 incident (flag reuse) — referenced by Sweep / Latch

Source: SKILL.md on GitHub

1 warning5mo5 checks · Risk SAFE
  • Gen Agent Trust Hub5mo

    The skill is a specialized tool for managing Claude Code hooks. While it provides significant security benefits through pre-built safety recipes for secret detection and command guarding, it inherently handles the generation and configuration of executable shell scripts. This creates a surface for indirect prompt injection if the agent is directed to process malicious project-level configuration files.

  • Socket5mo

    No alerts

  • Snyk5mo

    Risk: LOW · No issues

  • Runlayer6mo

    3/5 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 16a3f28. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Activeupdated last month

README badge

README badge for simota/agent-skills/latch