@claude-code-mastery/starter-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/.starter-kit/profiles/clean.md +113 -0
- package/.claude/.starter-kit/profiles/go.md +458 -0
- package/.claude/.starter-kit/profiles/node.md +429 -0
- package/.claude/.starter-kit/profiles/python.md +475 -0
- package/.claude/.starter-kit/shared/analytics-rybbit.md +55 -0
- package/.claude/.starter-kit/shared/claude-md-base.md +93 -0
- package/.claude/.starter-kit/shared/deployment-dokploy.md +158 -0
- package/.claude/.starter-kit/shared/feature-manifest.md +43 -0
- package/.claude/.starter-kit/shared/mcp-and-pooler.md +38 -0
- package/.claude/.starter-kit/shared/mongo-setup.md +20 -0
- package/.claude/.starter-kit/shared/profile-config.md +65 -0
- package/.claude/.starter-kit/shared/seo.md +113 -0
- package/.claude/.starter-kit/shared/sql-setup.md +37 -0
- package/.claude/commands/add-feature.md +349 -0
- package/.claude/commands/add-project-setup.md +156 -0
- package/.claude/commands/architecture.md +27 -0
- package/.claude/commands/commit.md +61 -0
- package/.claude/commands/convert-project-to-starter-kit.md +508 -0
- package/.claude/commands/create-api.md +385 -0
- package/.claude/commands/create-e2e.md +230 -0
- package/.claude/commands/diagram.md +301 -0
- package/.claude/commands/help.md +120 -0
- package/.claude/commands/install-global.md +145 -0
- package/.claude/commands/new-project.md +244 -0
- package/.claude/commands/optimize-docker.md +352 -0
- package/.claude/commands/progress.md +61 -0
- package/.claude/commands/projects-created.md +79 -0
- package/.claude/commands/quickstart.md +105 -0
- package/.claude/commands/refactor.md +267 -0
- package/.claude/commands/remove-project.md +95 -0
- package/.claude/commands/review.md +59 -0
- package/.claude/commands/security-check.md +77 -0
- package/.claude/commands/set-project-profile-default.md +79 -0
- package/.claude/commands/setup.md +337 -0
- package/.claude/commands/show-user-guide.md +58 -0
- package/.claude/commands/starter-kit.md +90 -0
- package/.claude/commands/test-plan.md +118 -0
- package/.claude/commands/update-project.md +413 -0
- package/.claude/commands/what-is-my-ai-doing.md +42 -0
- package/.claude/commands/worktree.md +124 -0
- package/.claude/hooks/block-dangerous-bash.py +55 -0
- package/.claude/hooks/check-branch.sh +116 -0
- package/.claude/hooks/check-e2e.sh +71 -0
- package/.claude/hooks/check-env-sync.sh +41 -0
- package/.claude/hooks/check-file-length.py +47 -0
- package/.claude/hooks/check-ports.sh +59 -0
- package/.claude/hooks/check-rulecatch.sh +33 -0
- package/.claude/hooks/check-rybbit.sh +63 -0
- package/.claude/hooks/lint-on-save.sh +59 -0
- package/.claude/hooks/verify-no-secrets.sh +80 -0
- package/.claude/settings.json +34 -0
- package/.claude/skills/api-conventions/SKILL.md +34 -0
- package/.claude/skills/code-review/SKILL.md +87 -0
- package/.claude/skills/code-review/references/mongodb-checks.md +25 -0
- package/.claude/skills/code-review/references/project-checks.md +38 -0
- package/.claude/skills/create-service/SKILL.md +222 -0
- package/.claude/skills/debugger/SKILL.md +39 -0
- package/.claude/skills/dependency-vetting/SKILL.md +46 -0
- package/.claude/skills/design-review/SKILL.md +50 -0
- package/.claude/skills/mcp-builder/SKILL.md +57 -0
- package/.claude/skills/mongodb-rules/SKILL.md +62 -0
- package/.claude/skills/terminal-tui/SKILL.md +106 -0
- package/.claude/skills/test-writer/SKILL.md +78 -0
- package/LICENSE +21 -0
- package/README.md +2152 -0
- package/bin/cli.js +205 -0
- package/claude-mastery-project.conf +220 -0
- package/global-claude-md/CLAUDE.md +212 -0
- package/global-claude-md/settings.json +3 -0
- package/package.json +81 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Branch Protection Hook — PreToolUse (Bash)
|
|
3
|
+
# Blocks committing directly to main/master when auto_branch is enabled.
|
|
4
|
+
# Exit code 2 = block operation and tell Claude why.
|
|
5
|
+
#
|
|
6
|
+
# Based on Claude Code Mastery Guides V1-V5 by TheDecipherist
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
|
|
10
|
+
# Extract command from JSON — try jq first, fall back to grep/sed (no Python needed)
|
|
11
|
+
if command -v jq &>/dev/null; then
|
|
12
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""' 2>/dev/null)
|
|
13
|
+
else
|
|
14
|
+
# Lightweight fallback: extract "command" value from JSON via grep/sed
|
|
15
|
+
COMMAND=$(echo "$INPUT" | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*:[[:space:]]*"//;s/"$//')
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if [ -z "$COMMAND" ]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Only check git commit commands
|
|
23
|
+
if ! echo "$COMMAND" | grep -qE 'git\s+commit'; then
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# Detect target directory from git -C <path> in the command
|
|
28
|
+
# Only match real paths (starting with / ~ . or alphanumeric), not placeholders like <dir>
|
|
29
|
+
TARGET_DIR=""
|
|
30
|
+
if echo "$COMMAND" | grep -qE 'git\s+-C\s+[/~.\w]'; then
|
|
31
|
+
TARGET_DIR=$(echo "$COMMAND" | sed -nE 's/.*git\s+-C\s+([^ ]+)\s+.*/\1/p' | head -1)
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Also detect: cd /some/path && git commit (common cross-repo pattern)
|
|
35
|
+
# Extract the first cd target from commands like "cd /path && git ..."
|
|
36
|
+
if [ -z "$TARGET_DIR" ] && echo "$COMMAND" | grep -qE '^cd\s+[^ ]+'; then
|
|
37
|
+
CD_DIR=$(echo "$COMMAND" | sed -nE 's/^cd\s+([^ &;]+).*/\1/p' | head -1)
|
|
38
|
+
# Expand leading tilde — [ -d "~/path" ] doesn't expand tilde inside quotes
|
|
39
|
+
CD_DIR="${CD_DIR/#\~/$HOME}"
|
|
40
|
+
if [ -n "$CD_DIR" ] && [ -d "$CD_DIR" ]; then
|
|
41
|
+
TARGET_DIR="$CD_DIR"
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Resolve git dir — if git -C was used with a valid path, check THAT repo
|
|
46
|
+
if [ -n "$TARGET_DIR" ] && [ -d "$TARGET_DIR" ]; then
|
|
47
|
+
if ! git -C "$TARGET_DIR" rev-parse --is-inside-work-tree &>/dev/null; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
BRANCH=$(git -C "$TARGET_DIR" branch --show-current 2>/dev/null)
|
|
51
|
+
# Allow initial commits (no previous commits in the target repo)
|
|
52
|
+
if ! git -C "$TARGET_DIR" rev-parse HEAD &>/dev/null 2>&1; then
|
|
53
|
+
exit 0
|
|
54
|
+
fi
|
|
55
|
+
else
|
|
56
|
+
# CWD-based git check
|
|
57
|
+
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
|
58
|
+
exit 0
|
|
59
|
+
fi
|
|
60
|
+
BRANCH=$(git branch --show-current 2>/dev/null)
|
|
61
|
+
# Allow initial commits (no previous commits yet)
|
|
62
|
+
if ! git rev-parse HEAD &>/dev/null 2>&1; then
|
|
63
|
+
exit 0
|
|
64
|
+
fi
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Only care about main/master
|
|
68
|
+
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
# Check auto_branch setting (default: true)
|
|
73
|
+
# Look for claude-mastery-project.conf in the target repo first, then CWD.
|
|
74
|
+
# If neither has the conf file, this is not a starter kit project — skip the check.
|
|
75
|
+
AUTO_BRANCH="true"
|
|
76
|
+
CONF_FOUND=false
|
|
77
|
+
CONF="claude-mastery-project.conf"
|
|
78
|
+
|
|
79
|
+
# Determine where to look for the conf — target dir or CWD
|
|
80
|
+
CONF_SEARCH_DIR=""
|
|
81
|
+
if [ -n "$TARGET_DIR" ] && [ -d "$TARGET_DIR" ]; then
|
|
82
|
+
CONF_SEARCH_DIR="$TARGET_DIR"
|
|
83
|
+
else
|
|
84
|
+
CONF_SEARCH_DIR="."
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
if [ -f "${CONF_SEARCH_DIR}/${CONF}" ]; then
|
|
88
|
+
CONF_FOUND=true
|
|
89
|
+
SETTING=$(grep -E '^\s*auto_branch\s*=' "${CONF_SEARCH_DIR}/${CONF}" 2>/dev/null | head -1 | sed 's/.*=\s*//' | sed 's/\s*#.*//' | tr -d ' ')
|
|
90
|
+
if [ -n "$SETTING" ]; then
|
|
91
|
+
AUTO_BRANCH="$SETTING"
|
|
92
|
+
fi
|
|
93
|
+
elif [ -z "$TARGET_DIR" ] && [ -f "$CONF" ]; then
|
|
94
|
+
# Fallback: conf exists in CWD — only when no cross-repo target was detected
|
|
95
|
+
CONF_FOUND=true
|
|
96
|
+
SETTING=$(grep -E '^\s*auto_branch\s*=' "$CONF" 2>/dev/null | head -1 | sed 's/.*=\s*//' | sed 's/\s*#.*//' | tr -d ' ')
|
|
97
|
+
if [ -n "$SETTING" ]; then
|
|
98
|
+
AUTO_BRANCH="$SETTING"
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
|
|
102
|
+
# If no starter-kit conf found anywhere, this isn't a starter-kit project — allow commit
|
|
103
|
+
if [ "$CONF_FOUND" = false ]; then
|
|
104
|
+
exit 0
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if [ "$AUTO_BRANCH" = "true" ]; then
|
|
108
|
+
echo "BLOCKED: You're committing directly to '$BRANCH' with auto_branch enabled." >&2
|
|
109
|
+
echo "Create a feature branch first:" >&2
|
|
110
|
+
echo " git checkout -b feat/<feature-name>" >&2
|
|
111
|
+
echo " Or use: /worktree <name>" >&2
|
|
112
|
+
exit 2
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# auto_branch is explicitly false — user chose to work on main
|
|
116
|
+
exit 0
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# E2E Test Check Hook — PreToolUse (Bash)
|
|
3
|
+
# Warns before pushing to main if no real E2E tests exist.
|
|
4
|
+
# Exit code 2 = block operation and tell Claude why.
|
|
5
|
+
#
|
|
6
|
+
# Based on Claude Code Mastery Guides V1-V5 by TheDecipherist
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
|
|
10
|
+
|
|
11
|
+
if [ -z "$COMMAND" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Only check git push commands
|
|
16
|
+
if ! echo "$COMMAND" | grep -qE 'git\s+push'; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Must be in a git repo
|
|
21
|
+
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Determine if pushing to main/master
|
|
26
|
+
BRANCH=$(git branch --show-current 2>/dev/null)
|
|
27
|
+
PUSHING_TO_MAIN=false
|
|
28
|
+
|
|
29
|
+
# Check if command explicitly targets main/master
|
|
30
|
+
if echo "$COMMAND" | grep -qE 'git\s+push\s+\S+\s+(main|master)'; then
|
|
31
|
+
PUSHING_TO_MAIN=true
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Check if current branch is main/master (push without explicit branch)
|
|
35
|
+
if [ "$BRANCH" = "main" ] || [ "$BRANCH" = "master" ]; then
|
|
36
|
+
if ! echo "$COMMAND" | grep -qE 'git\s+push\s+\S+\s+\S+'; then
|
|
37
|
+
# No explicit branch in command — pushing current branch
|
|
38
|
+
PUSHING_TO_MAIN=true
|
|
39
|
+
fi
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if [ "$PUSHING_TO_MAIN" = "false" ]; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
# Check for real E2E test files (excluding the example template)
|
|
47
|
+
E2E_DIR="tests/e2e"
|
|
48
|
+
if [ ! -d "$E2E_DIR" ]; then
|
|
49
|
+
echo "WARNING: No tests/e2e/ directory found." >&2
|
|
50
|
+
echo "Consider creating E2E tests: /create-e2e <feature>" >&2
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Count .spec.ts and .test.ts files, excluding the example template
|
|
55
|
+
REAL_TESTS=$(find "$E2E_DIR" -name "*.spec.ts" -o -name "*.test.ts" 2>/dev/null \
|
|
56
|
+
| grep -v "example-homepage.spec.ts" \
|
|
57
|
+
| wc -l | tr -d ' ')
|
|
58
|
+
|
|
59
|
+
# Also check for Go test files
|
|
60
|
+
GO_TESTS=$(find "$E2E_DIR" -name '*_test.go' 2>/dev/null | wc -l | tr -d ' ')
|
|
61
|
+
if [ "$REAL_TESTS" -eq 0 ] && [ "$GO_TESTS" -gt 0 ]; then
|
|
62
|
+
REAL_TESTS=$GO_TESTS
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
if [ "$REAL_TESTS" -eq 0 ]; then
|
|
66
|
+
echo "WARNING: No E2E tests found (only the example template exists)." >&2
|
|
67
|
+
echo "Consider creating E2E tests: /create-e2e <feature>" >&2
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
exit 0
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Env Example Sync Hook — Stop
|
|
3
|
+
# Warns if .env has keys that .env.example doesn't document.
|
|
4
|
+
# SECURITY: Only reads key NAMES, never values.
|
|
5
|
+
#
|
|
6
|
+
# Based on Claude Code Mastery Guides V1-V5 by TheDecipherist
|
|
7
|
+
|
|
8
|
+
# Both files must exist
|
|
9
|
+
if [ ! -f ".env" ] || [ ! -f ".env.example" ]; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Extract sorted key names from a file (ignoring comments, blank lines, export prefix)
|
|
14
|
+
extract_keys() {
|
|
15
|
+
grep -E '^(export\s+)?[A-Za-z_][A-Za-z0-9_]*=' "$1" 2>/dev/null \
|
|
16
|
+
| sed 's/^export\s*//' \
|
|
17
|
+
| cut -d'=' -f1 \
|
|
18
|
+
| sort -u
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ENV_KEYS=$(extract_keys .env)
|
|
22
|
+
EXAMPLE_KEYS=$(extract_keys .env.example)
|
|
23
|
+
|
|
24
|
+
# Find keys in .env that are missing from .env.example
|
|
25
|
+
MISSING=""
|
|
26
|
+
while IFS= read -r key; do
|
|
27
|
+
if [ -n "$key" ] && ! echo "$EXAMPLE_KEYS" | grep -qx "$key"; then
|
|
28
|
+
MISSING="${MISSING}\n - $key"
|
|
29
|
+
fi
|
|
30
|
+
done <<< "$ENV_KEYS"
|
|
31
|
+
|
|
32
|
+
if [ -n "$MISSING" ]; then
|
|
33
|
+
echo "" >&2
|
|
34
|
+
echo "ENV SYNC: Keys in .env missing from .env.example:" >&2
|
|
35
|
+
echo -e "$MISSING" >&2
|
|
36
|
+
echo "" >&2
|
|
37
|
+
echo "Other developers won't know these variables exist." >&2
|
|
38
|
+
echo "Add them to .env.example with placeholder values." >&2
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
exit 0
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse (Write|Edit) — enforce the 300-line file limit.
|
|
3
|
+
|
|
4
|
+
The write already happened (PostToolUse can't undo it), so exit 2 feeds the file
|
|
5
|
+
back to Claude as a "split this now" instruction before it moves on. Only counts
|
|
6
|
+
real source files; skips generated and vendored paths.
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
LIMIT = 300
|
|
13
|
+
CODE_EXT = (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs", ".java")
|
|
14
|
+
SKIP_SUBSTR = ("/node_modules/", "/dist/", "/build/", "/.next/", "/coverage/", "/.git/")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
try:
|
|
19
|
+
data = json.load(sys.stdin)
|
|
20
|
+
except Exception:
|
|
21
|
+
sys.exit(0)
|
|
22
|
+
|
|
23
|
+
path = (data.get("tool_input", {}) or {}).get("file_path", "") or ""
|
|
24
|
+
if not path or not path.endswith(CODE_EXT):
|
|
25
|
+
sys.exit(0)
|
|
26
|
+
if any(s in path for s in SKIP_SUBSTR):
|
|
27
|
+
sys.exit(0)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
|
31
|
+
n = sum(1 for _ in f)
|
|
32
|
+
except OSError:
|
|
33
|
+
sys.exit(0)
|
|
34
|
+
|
|
35
|
+
if n > LIMIT:
|
|
36
|
+
sys.stderr.write(
|
|
37
|
+
os.path.basename(path) + " is now " + str(n) + " lines, over the "
|
|
38
|
+
+ str(LIMIT) + "-line limit. Split it by concern (one responsibility "
|
|
39
|
+
"per file) before moving on.\n"
|
|
40
|
+
)
|
|
41
|
+
sys.exit(2)
|
|
42
|
+
|
|
43
|
+
sys.exit(0)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Port Conflict Check Hook — PreToolUse (Bash)
|
|
3
|
+
# Blocks starting a server when the target port is already in use.
|
|
4
|
+
# Exit code 2 = block operation and tell Claude why.
|
|
5
|
+
#
|
|
6
|
+
# Based on Claude Code Mastery Guides V1-V5 by TheDecipherist
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
|
|
10
|
+
|
|
11
|
+
if [ -z "$COMMAND" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
PORT=""
|
|
16
|
+
|
|
17
|
+
# 1. Explicit port flags: -p 3000, --port 3000, --port=3000
|
|
18
|
+
if echo "$COMMAND" | grep -qoE '(-p|--port[= ])\s*[0-9]+'; then
|
|
19
|
+
PORT=$(echo "$COMMAND" | grep -oE '(-p|--port[= ])\s*[0-9]+' | grep -oE '[0-9]+' | head -1)
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# 2. PORT= environment variable prefix
|
|
23
|
+
if [ -z "$PORT" ] && echo "$COMMAND" | grep -qE 'PORT=[0-9]+'; then
|
|
24
|
+
PORT=$(echo "$COMMAND" | grep -oE 'PORT=[0-9]+' | head -1 | cut -d'=' -f2)
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# 3. Known pnpm script names -> known ports
|
|
28
|
+
if [ -z "$PORT" ]; then
|
|
29
|
+
case "$COMMAND" in
|
|
30
|
+
*dev:test:website*) PORT=4000 ;;
|
|
31
|
+
*dev:test:api*) PORT=4010 ;;
|
|
32
|
+
*dev:test:dashboard*) PORT=4020 ;;
|
|
33
|
+
*dev:website*) PORT=3000 ;;
|
|
34
|
+
*dev:api*) PORT=3001 ;;
|
|
35
|
+
*dev:dashboard*) PORT=3002 ;;
|
|
36
|
+
esac
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# No port detected — nothing to check
|
|
40
|
+
if [ -z "$PORT" ]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Check if lsof is available
|
|
45
|
+
if ! command -v lsof &>/dev/null; then
|
|
46
|
+
exit 0
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Check if port is in use
|
|
50
|
+
PID=$(lsof -ti:"$PORT" 2>/dev/null | head -1)
|
|
51
|
+
|
|
52
|
+
if [ -n "$PID" ]; then
|
|
53
|
+
PROC=$(ps -p "$PID" -o comm= 2>/dev/null || echo "unknown")
|
|
54
|
+
echo "BLOCKED: Port $PORT is already in use by $PROC (PID: $PID)." >&2
|
|
55
|
+
echo "Kill it first: lsof -ti:$PORT | xargs kill -9" >&2
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
exit 0
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# RuleCatch Check Hook — Stop
|
|
3
|
+
# Runs when Claude finishes a turn — reports any rule violations detected.
|
|
4
|
+
# Guaranteed to run (stronger than CLAUDE.md command instructions).
|
|
5
|
+
#
|
|
6
|
+
# Based on Claude Code Mastery Guides V1-V5 by TheDecipherist
|
|
7
|
+
|
|
8
|
+
# Check if RuleCatch CLI is available
|
|
9
|
+
if ! command -v npx &>/dev/null; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Check if @rulecatch/ai-pooler is available (quick check)
|
|
14
|
+
# If not installed, skip silently — don't block the user
|
|
15
|
+
# Always use latest to make sure the ai-pooler is up to date
|
|
16
|
+
if ! npx @rulecatch/ai-pooler@latest check --help &>/dev/null 2>&1; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Run RuleCatch violation check for the current session
|
|
21
|
+
# --quiet: only output if violations found
|
|
22
|
+
# --format: short summary suitable for hook output
|
|
23
|
+
RESULT=$(npx @rulecatch/ai-pooler@latest check --quiet --format summary 2>/dev/null)
|
|
24
|
+
|
|
25
|
+
if [ -n "$RESULT" ] && [ "$RESULT" != "0 violations" ]; then
|
|
26
|
+
echo "" >&2
|
|
27
|
+
echo "📋 RuleCatch: $RESULT" >&2
|
|
28
|
+
echo " Run 'pnpm ai:monitor' for details or check your dashboard." >&2
|
|
29
|
+
# Exit 0 = inform but don't block (violations are warnings, not blockers)
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
exit 0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Rybbit Pre-Deploy Check Hook — PreToolUse (Bash)
|
|
3
|
+
# Blocks deployment commands when Rybbit analytics is configured but not set up.
|
|
4
|
+
# Exit code 2 = block operation and tell Claude why.
|
|
5
|
+
#
|
|
6
|
+
# Based on Claude Code Mastery Guides V1-V5 by TheDecipherist
|
|
7
|
+
|
|
8
|
+
INPUT=$(cat)
|
|
9
|
+
COMMAND=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
|
|
10
|
+
|
|
11
|
+
if [ -z "$COMMAND" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
# Skip commands that are git operations — commits, adds, etc. are never deployments
|
|
16
|
+
if echo "$COMMAND" | grep -qE 'git\s+(commit|add|push|merge|rebase|checkout|branch|tag|log|diff|status)'; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Only check actual deployment commands (not file writes that mention deployment keywords)
|
|
21
|
+
# - docker push: actual push to registry
|
|
22
|
+
# - vercel deploy/--prod: actual Vercel deploy
|
|
23
|
+
# - curl.*application.deploy: actual Dokploy API call
|
|
24
|
+
# Avoids false positives from heredocs, cat, echo, python file writes, etc.
|
|
25
|
+
if ! echo "$COMMAND" | grep -qEi '(docker\s+push|vercel\s+deploy|vercel\s+--prod|curl.*application\.deploy)'; then
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Check if project uses Rybbit
|
|
30
|
+
CONF="claude-mastery-project.conf"
|
|
31
|
+
if [ ! -f "$CONF" ]; then
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if ! grep -q 'analytics\s*=\s*rybbit' "$CONF" 2>/dev/null; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Project uses Rybbit — verify it's configured
|
|
40
|
+
if [ ! -f ".env" ]; then
|
|
41
|
+
echo "BLOCKED: Rybbit analytics is configured in this project but .env file is missing." >&2
|
|
42
|
+
echo "Create .env with NEXT_PUBLIC_RYBBIT_SITE_ID=<your-site-id> before deploying." >&2
|
|
43
|
+
echo "Get your site ID from https://app.rybbit.io" >&2
|
|
44
|
+
exit 2
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
SITE_ID=$(grep -E '^NEXT_PUBLIC_RYBBIT_SITE_ID=' .env 2>/dev/null | head -1 | cut -d'=' -f2- | tr -d ' "'"'"'')
|
|
48
|
+
|
|
49
|
+
if [ -z "$SITE_ID" ]; then
|
|
50
|
+
echo "BLOCKED: NEXT_PUBLIC_RYBBIT_SITE_ID is missing from .env." >&2
|
|
51
|
+
echo "Add NEXT_PUBLIC_RYBBIT_SITE_ID=<your-site-id> to .env before deploying." >&2
|
|
52
|
+
echo "Get your site ID from https://app.rybbit.io" >&2
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Check for placeholder values
|
|
57
|
+
if echo "$SITE_ID" | grep -qEi '(your_site|placeholder|changeme|your-site|example)'; then
|
|
58
|
+
echo "BLOCKED: NEXT_PUBLIC_RYBBIT_SITE_ID appears to be a placeholder ('$SITE_ID')." >&2
|
|
59
|
+
echo "Set a real site ID from https://app.rybbit.io before deploying." >&2
|
|
60
|
+
exit 2
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
exit 0
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Lint on Save Hook — PostToolUse
|
|
3
|
+
# Runs linter after Claude writes/edits a file.
|
|
4
|
+
# Keep hooks FAST (<5 seconds) or Claude may not wait (V5 lesson).
|
|
5
|
+
#
|
|
6
|
+
# Based on Claude Code Mastery Guides V1-V5 by TheDecipherist
|
|
7
|
+
|
|
8
|
+
# Read the tool input from stdin
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null)
|
|
11
|
+
|
|
12
|
+
if [ -z "$FILE_PATH" ]; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
EXTENSION="${FILE_PATH##*.}"
|
|
17
|
+
|
|
18
|
+
case "$EXTENSION" in
|
|
19
|
+
ts|tsx)
|
|
20
|
+
# TypeScript — run type check on the specific file if tsc is available
|
|
21
|
+
if command -v npx &> /dev/null && [ -f "tsconfig.json" ]; then
|
|
22
|
+
npx tsc --noEmit --pretty "$FILE_PATH" 2>&1 | head -20
|
|
23
|
+
fi
|
|
24
|
+
;;
|
|
25
|
+
js|jsx)
|
|
26
|
+
# JavaScript — run eslint if available
|
|
27
|
+
if command -v npx &> /dev/null && [ -f ".eslintrc*" ] || [ -f "eslint.config.*" ]; then
|
|
28
|
+
npx eslint --no-error-on-unmatched-pattern "$FILE_PATH" 2>&1 | head -20
|
|
29
|
+
fi
|
|
30
|
+
;;
|
|
31
|
+
py)
|
|
32
|
+
# Python — run ruff or flake8 if available
|
|
33
|
+
if command -v ruff &> /dev/null; then
|
|
34
|
+
ruff check "$FILE_PATH" 2>&1 | head -20
|
|
35
|
+
elif command -v flake8 &> /dev/null; then
|
|
36
|
+
flake8 "$FILE_PATH" 2>&1 | head -20
|
|
37
|
+
fi
|
|
38
|
+
;;
|
|
39
|
+
vue)
|
|
40
|
+
# Vue — run vue-tsc if available
|
|
41
|
+
if command -v npx &> /dev/null && [ -f "tsconfig.json" ]; then
|
|
42
|
+
npx vue-tsc --noEmit 2>&1 | head -20
|
|
43
|
+
fi
|
|
44
|
+
;;
|
|
45
|
+
svelte)
|
|
46
|
+
# Svelte — run svelte-check if available
|
|
47
|
+
if command -v npx &> /dev/null; then
|
|
48
|
+
npx svelte-check --tsconfig ./tsconfig.json 2>&1 | head -20
|
|
49
|
+
fi
|
|
50
|
+
;;
|
|
51
|
+
go)
|
|
52
|
+
# Go — run go vet if available
|
|
53
|
+
if command -v go &> /dev/null; then
|
|
54
|
+
go vet "$FILE_PATH" 2>&1 | head -20
|
|
55
|
+
fi
|
|
56
|
+
;;
|
|
57
|
+
esac
|
|
58
|
+
|
|
59
|
+
exit 0
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Verify No Secrets Hook — Stop
|
|
3
|
+
# Checks staged git files for accidentally committed secrets.
|
|
4
|
+
# Runs when Claude finishes a turn — catches secrets before they're committed.
|
|
5
|
+
#
|
|
6
|
+
# Based on Claude Code Mastery Guides V1-V5 by TheDecipherist
|
|
7
|
+
|
|
8
|
+
# Only run if we're in a git repo
|
|
9
|
+
if ! git rev-parse --is-inside-work-tree &>/dev/null 2>&1; then
|
|
10
|
+
exit 0
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Check if there are staged files
|
|
14
|
+
STAGED=$(git diff --cached --name-only 2>/dev/null)
|
|
15
|
+
if [ -z "$STAGED" ]; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
VIOLATIONS=""
|
|
20
|
+
|
|
21
|
+
# Check for sensitive files being staged (match basename only — anchored)
|
|
22
|
+
SENSITIVE_BASENAMES=".env .env.local .env.production .env.staging secrets.json credentials.json service-account.json .npmrc"
|
|
23
|
+
for pattern in $SENSITIVE_BASENAMES; do
|
|
24
|
+
while IFS= read -r file; do
|
|
25
|
+
basename=$(basename "$file")
|
|
26
|
+
if [ "$basename" = "$pattern" ]; then
|
|
27
|
+
VIOLATIONS="${VIOLATIONS}\n - SENSITIVE FILE STAGED: $file"
|
|
28
|
+
fi
|
|
29
|
+
done <<< "$STAGED"
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
# Check for private key files (anchored to basename)
|
|
33
|
+
while IFS= read -r file; do
|
|
34
|
+
basename=$(basename "$file")
|
|
35
|
+
case "$basename" in
|
|
36
|
+
id_rsa|id_ed25519|id_ecdsa|id_dsa|*.pem|*.key)
|
|
37
|
+
VIOLATIONS="${VIOLATIONS}\n - PRIVATE KEY FILE STAGED: $file"
|
|
38
|
+
;;
|
|
39
|
+
esac
|
|
40
|
+
done <<< "$STAGED"
|
|
41
|
+
|
|
42
|
+
# Check staged file contents for common secret patterns
|
|
43
|
+
while IFS= read -r file; do
|
|
44
|
+
if [ -f "$file" ]; then
|
|
45
|
+
# Generic API key / secret / password / token patterns
|
|
46
|
+
if grep -qEi '(api[_-]?key|secret[_-]?key|password|token)\s*[:=]\s*["\x27][A-Za-z0-9+/=_-]{16,}' "$file" 2>/dev/null; then
|
|
47
|
+
VIOLATIONS="${VIOLATIONS}\n - POSSIBLE SECRET in $file"
|
|
48
|
+
fi
|
|
49
|
+
# AWS access keys
|
|
50
|
+
if grep -qE 'AKIA[0-9A-Z]{16}' "$file" 2>/dev/null; then
|
|
51
|
+
VIOLATIONS="${VIOLATIONS}\n - AWS ACCESS KEY in $file"
|
|
52
|
+
fi
|
|
53
|
+
# GitHub tokens (ghp_, gho_, ghs_, ghr_, github_pat_)
|
|
54
|
+
if grep -qE '(ghp_[A-Za-z0-9]{36,}|gho_[A-Za-z0-9]{36,}|ghs_[A-Za-z0-9]{36,}|ghr_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{22,})' "$file" 2>/dev/null; then
|
|
55
|
+
VIOLATIONS="${VIOLATIONS}\n - GITHUB TOKEN in $file"
|
|
56
|
+
fi
|
|
57
|
+
# Slack tokens
|
|
58
|
+
if grep -qE '(xoxb-|xoxp-|xoxo-|xoxa-)[0-9A-Za-z-]{20,}' "$file" 2>/dev/null; then
|
|
59
|
+
VIOLATIONS="${VIOLATIONS}\n - SLACK TOKEN in $file"
|
|
60
|
+
fi
|
|
61
|
+
# Stripe keys
|
|
62
|
+
if grep -qE '(sk_live_|pk_live_|rk_live_)[A-Za-z0-9]{20,}' "$file" 2>/dev/null; then
|
|
63
|
+
VIOLATIONS="${VIOLATIONS}\n - STRIPE KEY in $file"
|
|
64
|
+
fi
|
|
65
|
+
# PEM-format private keys
|
|
66
|
+
if grep -qE '-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----' "$file" 2>/dev/null; then
|
|
67
|
+
VIOLATIONS="${VIOLATIONS}\n - PEM PRIVATE KEY in $file"
|
|
68
|
+
fi
|
|
69
|
+
fi
|
|
70
|
+
done <<< "$STAGED"
|
|
71
|
+
|
|
72
|
+
if [ -n "$VIOLATIONS" ]; then
|
|
73
|
+
echo -e "⚠️ POTENTIAL SECRETS DETECTED:${VIOLATIONS}" >&2
|
|
74
|
+
echo "" >&2
|
|
75
|
+
echo "Review staged files before committing." >&2
|
|
76
|
+
# Exit 2 = block and inform Claude
|
|
77
|
+
exit 2
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
exit 0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{ "type": "command", "command": "bash .claude/hooks/check-rybbit.sh" },
|
|
8
|
+
{ "type": "command", "command": "bash .claude/hooks/check-branch.sh" },
|
|
9
|
+
{ "type": "command", "command": "bash .claude/hooks/check-ports.sh" },
|
|
10
|
+
{ "type": "command", "command": "bash .claude/hooks/check-e2e.sh" },
|
|
11
|
+
{ "type": "command", "command": "python3 .claude/hooks/block-dangerous-bash.py" }
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"PostToolUse": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "Write|Edit",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{ "type": "command", "command": "bash .claude/hooks/lint-on-save.sh" },
|
|
20
|
+
{ "type": "command", "command": "python3 .claude/hooks/check-file-length.py" }
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
],
|
|
24
|
+
"Stop": [
|
|
25
|
+
{
|
|
26
|
+
"hooks": [
|
|
27
|
+
{ "type": "command", "command": "bash .claude/hooks/verify-no-secrets.sh" },
|
|
28
|
+
{ "type": "command", "command": "bash .claude/hooks/check-rulecatch.sh" },
|
|
29
|
+
{ "type": "command", "command": "bash .claude/hooks/check-env-sync.sh" }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-conventions
|
|
3
|
+
description: Service structure conventions for this codebase: routing, layering, and where code lives. Use whenever writing or modifying a service's server entry, route definitions, handlers, or adapters. Loads inline so it shapes structure as code is written.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Service Architecture Conventions
|
|
7
|
+
|
|
8
|
+
Every service is three layers, one direction: `server.ts` → `handlers/` → `adapters/`. Apply while writing, not after.
|
|
9
|
+
|
|
10
|
+
## Versioning
|
|
11
|
+
|
|
12
|
+
- All routes live under `/api/v1/`. New endpoints are versioned from the first line. No unversioned routes "for now."
|
|
13
|
+
|
|
14
|
+
## server.ts is thin
|
|
15
|
+
|
|
16
|
+
- `server.ts` defines routes and delegates. Nothing else.
|
|
17
|
+
- No business logic in `server.ts` or in a route definition. A route wires the request to a handler and returns its result.
|
|
18
|
+
|
|
19
|
+
## handlers/ hold the logic
|
|
20
|
+
|
|
21
|
+
- Business logic lives in `handlers/`, one file per domain.
|
|
22
|
+
- A handler owns its domain's rules and orchestration. It calls adapters for anything external. It does not reach outside the process itself.
|
|
23
|
+
|
|
24
|
+
## adapters/ wrap everything external
|
|
25
|
+
|
|
26
|
+
- Database, external APIs, queues, anything outside the process goes through an adapter in `adapters/`. Handlers never touch them directly.
|
|
27
|
+
- **The database adapter uses StrictDB when it's installed, otherwise the native driver. Never Mongoose.** A handler that imports a driver or calls an external API inline is wrong, that belongs in an adapter. The data adapter is the one place driver code lives, which is also where the `mongodb-rules` apply.
|
|
28
|
+
|
|
29
|
+
## Service (package) separation
|
|
30
|
+
|
|
31
|
+
- A service owns its domain and is reached through its interface. A package does not reach into another package's internals or its data. Call the owning service.
|
|
32
|
+
- Code two services both need is hoisted to a shared layer, never imported sideways from a sibling.
|
|
33
|
+
|
|
34
|
+
The test: routes in `server.ts` read request-in, handler-call, response-out. Logic sits in `handlers/`. Anything that leaves the process goes through `adapters/`, and the data adapter is StrictDB.
|