forge-orkes 0.42.0 → 0.44.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.
Files changed (32) hide show
  1. package/bin/create-forge.js +138 -1
  2. package/package.json +1 -1
  3. package/template/.claude/agents/doc-reviewer.md +115 -0
  4. package/template/.claude/agents/performance-reviewer.md +138 -0
  5. package/template/.claude/agents/security-reviewer.md +163 -0
  6. package/template/.claude/hooks/README.md +39 -0
  7. package/template/.claude/hooks/block-dangerous-commands.sh +158 -0
  8. package/template/.claude/hooks/forge-active-skill-guard.sh +44 -0
  9. package/template/.claude/hooks/format-on-save.sh +95 -0
  10. package/template/.claude/hooks/protect-files.sh +101 -0
  11. package/template/.claude/hooks/scan-secrets.sh +87 -0
  12. package/template/.claude/hooks/tests/README.md +76 -0
  13. package/template/.claude/hooks/tests/cases/block-dangerous-commands.cases.json +376 -0
  14. package/template/.claude/hooks/tests/cases/protect-files.cases.json +222 -0
  15. package/template/.claude/hooks/tests/cases/scan-secrets.cases.json +218 -0
  16. package/template/.claude/hooks/tests/cases/warn-large-files.cases.json +146 -0
  17. package/template/.claude/hooks/tests/run.sh +118 -0
  18. package/template/.claude/hooks/warn-large-files.sh +71 -0
  19. package/template/.claude/rules/README.md +63 -0
  20. package/template/.claude/rules/agent-discipline.md +14 -0
  21. package/template/.claude/settings.json +69 -1
  22. package/template/.claude/skills/architecting/SKILL.md +1 -1
  23. package/template/.claude/skills/chief-of-staff/SKILL.md +14 -0
  24. package/template/.claude/skills/executing/SKILL.md +16 -0
  25. package/template/.claude/skills/forge/SKILL.md +12 -1
  26. package/template/.claude/skills/initializing/SKILL.md +4 -0
  27. package/template/.claude/skills/planning/SKILL.md +13 -1
  28. package/template/.claude/skills/reviewing/SKILL.md +2 -0
  29. package/template/.claude/skills/verifying/SKILL.md +19 -8
  30. package/template/.forge/FORGE.md +16 -3
  31. package/template/.forge/migrations/0.43.0-safety-policy.md +60 -0
  32. package/template/.forge/migrations/0.44.0-desire-paths.md +57 -0
@@ -0,0 +1,218 @@
1
+ [
2
+ {
3
+ "name": "flags AWS access key ID on Write",
4
+ "input": {
5
+ "tool_name": "Write",
6
+ "tool_input": {
7
+ "file_path": "src/config.py",
8
+ "content": "AWS_KEY = 'AKIAIOSFODNN7EXAMPLE'"
9
+ }
10
+ },
11
+ "expect_exit": 2,
12
+ "expect_stdout_contains": "\"permissionDecision\":\"ask\""
13
+ },
14
+ {
15
+ "name": "flags AWS secret access key assignment",
16
+ "input": {
17
+ "tool_name": "Write",
18
+ "tool_input": {
19
+ "file_path": "src/config.py",
20
+ "content": "aws_secret_access_key = \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\""
21
+ }
22
+ },
23
+ "expect_exit": 2,
24
+ "expect_stdout_contains": "AWS secret key"
25
+ },
26
+ {
27
+ "name": "flags GitHub personal access token",
28
+ "input": {
29
+ "tool_name": "Write",
30
+ "tool_input": {
31
+ "file_path": "src/config.py",
32
+ "content": "GH_TOKEN = 'ghp_abcdefghijklmnopqrstuvwxyz0123456789'"
33
+ }
34
+ },
35
+ "expect_exit": 2,
36
+ "expect_stdout_contains": "GitHub token"
37
+ },
38
+ {
39
+ "name": "flags Anthropic API key",
40
+ "input": {
41
+ "tool_name": "Write",
42
+ "tool_input": {
43
+ "file_path": "src/config.py",
44
+ "content": "ANTHROPIC_API_KEY = 'sk-ant-abcdefghijklmnopqrstuv'"
45
+ }
46
+ },
47
+ "expect_exit": 2,
48
+ "expect_stdout_contains": "Anthropic API key"
49
+ },
50
+ {
51
+ "name": "flags Slack bot token",
52
+ "input": {
53
+ "tool_name": "Write",
54
+ "tool_input": {
55
+ "file_path": "src/config.py",
56
+ "content": "SLACK_TOKEN = 'xoxb-1234567890-abcdefghijk'"
57
+ }
58
+ },
59
+ "expect_exit": 2,
60
+ "expect_stdout_contains": "Slack token"
61
+ },
62
+ {
63
+ "name": "flags a private key block",
64
+ "input": {
65
+ "tool_name": "Write",
66
+ "tool_input": {
67
+ "file_path": "secrets/key.pem",
68
+ "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----"
69
+ }
70
+ },
71
+ "expect_exit": 2,
72
+ "expect_stdout_contains": "private key block"
73
+ },
74
+ {
75
+ "name": "flags Postgres connection string with embedded credentials",
76
+ "input": {
77
+ "tool_name": "Write",
78
+ "tool_input": {
79
+ "file_path": ".env.example",
80
+ "content": "DATABASE_URL=postgres://admin:s3cretP@ss@db.example.com:5432/app"
81
+ }
82
+ },
83
+ "expect_exit": 2,
84
+ "expect_stdout_contains": "connection string with credentials"
85
+ },
86
+ {
87
+ "name": "flags Laravel APP_KEY with real base64 value",
88
+ "input": {
89
+ "tool_name": "Write",
90
+ "tool_input": {
91
+ "file_path": ".env",
92
+ "content": "APP_KEY=base64:IAm40CharsOfBase64EncodedAppKeyForLaravelApp="
93
+ }
94
+ },
95
+ "expect_exit": 2,
96
+ "expect_stdout_contains": "Laravel APP_KEY"
97
+ },
98
+ {
99
+ "name": "flags hardcoded password assignment with literal value",
100
+ "input": {
101
+ "tool_name": "Write",
102
+ "tool_input": {
103
+ "file_path": "app/Services/MailService.php",
104
+ "content": "$password = \"realPassword123\";"
105
+ }
106
+ },
107
+ "expect_exit": 2,
108
+ "expect_stdout_contains": "hardcoded credential"
109
+ },
110
+ {
111
+ "name": "flags secret found inside an Edit new_string",
112
+ "input": {
113
+ "tool_name": "Edit",
114
+ "tool_input": {
115
+ "file_path": "src/config.py",
116
+ "old_string": "TOKEN = ''",
117
+ "new_string": "TOKEN = 'ghp_abcdefghijklmnopqrstuvwxyz0123456789'"
118
+ }
119
+ },
120
+ "expect_exit": 2,
121
+ "expect_stdout_contains": "GitHub token"
122
+ },
123
+ {
124
+ "name": "allows password field that references process.env",
125
+ "input": {
126
+ "tool_name": "Write",
127
+ "tool_input": {
128
+ "file_path": "src/db.js",
129
+ "content": "const password = process.env.DB_PASSWORD;"
130
+ }
131
+ },
132
+ "expect_exit": 0,
133
+ "expect_stdout_not_contains": "permissionDecision"
134
+ },
135
+ {
136
+ "name": "allows password field that uses Laravel env() helper",
137
+ "input": {
138
+ "tool_name": "Write",
139
+ "tool_input": {
140
+ "file_path": "config/database.php",
141
+ "content": "'password' => env('DB_PASSWORD', ''),"
142
+ }
143
+ },
144
+ "expect_exit": 0,
145
+ "expect_stdout_not_contains": "permissionDecision"
146
+ },
147
+ {
148
+ "name": "allows password mentioned only in a comment",
149
+ "input": {
150
+ "tool_name": "Write",
151
+ "tool_input": {
152
+ "file_path": "app/Services/MailService.php",
153
+ "content": "// Remember to set the MAIL_PASSWORD in .env\n$mailer = new Mailer();"
154
+ }
155
+ },
156
+ "expect_exit": 0,
157
+ "expect_stdout_not_contains": "permissionDecision"
158
+ },
159
+ {
160
+ "name": "allows empty content",
161
+ "input": {
162
+ "tool_name": "Write",
163
+ "tool_input": {
164
+ "file_path": "src/empty.py",
165
+ "content": ""
166
+ }
167
+ },
168
+ "expect_exit": 0,
169
+ "expect_stdout_not_contains": "permissionDecision"
170
+ },
171
+ {
172
+ "name": "ignores non-Write/Edit tools",
173
+ "input": {
174
+ "tool_name": "Bash",
175
+ "tool_input": {
176
+ "command": "echo ghp_abcdefghijklmnopqrstuvwxyz0123456789"
177
+ }
178
+ },
179
+ "expect_exit": 0,
180
+ "expect_stdout_not_contains": "permissionDecision"
181
+ },
182
+ {
183
+ "name": "allows an APP_KEY placeholder shorter than 40 chars",
184
+ "input": {
185
+ "tool_name": "Write",
186
+ "tool_input": {
187
+ "file_path": ".env.example",
188
+ "content": "APP_KEY=base64:TODO_RUN_ARTISAN_KEY_GENERATE"
189
+ }
190
+ },
191
+ "expect_exit": 0,
192
+ "expect_stdout_not_contains": "permissionDecision"
193
+ },
194
+ {
195
+ "name": "allows a regular Python file with no secrets",
196
+ "input": {
197
+ "tool_name": "Write",
198
+ "tool_input": {
199
+ "file_path": "src/utils.py",
200
+ "content": "def add(a: int, b: int) -> int:\n return a + b\n"
201
+ }
202
+ },
203
+ "expect_exit": 0,
204
+ "expect_stdout_not_contains": "permissionDecision"
205
+ },
206
+ {
207
+ "name": "flags a Google API key",
208
+ "input": {
209
+ "tool_name": "Write",
210
+ "tool_input": {
211
+ "file_path": "config.js",
212
+ "content": "const key = \"AIzaSyD-1234567890abcdefGHIJKLMNOPqrstuv\""
213
+ }
214
+ },
215
+ "expect_exit": 2,
216
+ "expect_stdout_contains": "Google API key"
217
+ }
218
+ ]
@@ -0,0 +1,146 @@
1
+ [
2
+ {
3
+ "name": "denies writing into node_modules",
4
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "node_modules/react/index.js" } },
5
+ "expect_exit": 2,
6
+ "expect_stdout_contains": "node_modules"
7
+ },
8
+ {
9
+ "name": "denies writing into nested node_modules",
10
+ "input": { "tool_name": "Edit", "tool_input": { "file_path": "apps/web/node_modules/foo/index.js" } },
11
+ "expect_exit": 2,
12
+ "expect_stdout_contains": "node_modules"
13
+ },
14
+ {
15
+ "name": "denies writing into dist",
16
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "dist/bundle.js" } },
17
+ "expect_exit": 2,
18
+ "expect_stdout_contains": "build output"
19
+ },
20
+ {
21
+ "name": "denies writing into build",
22
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "build/app.js" } },
23
+ "expect_exit": 2,
24
+ "expect_stdout_contains": "build output"
25
+ },
26
+ {
27
+ "name": "denies writing into vendor",
28
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "vendor/laravel/framework/src/foo.php" } },
29
+ "expect_exit": 2,
30
+ "expect_stdout_contains": "vendor"
31
+ },
32
+ {
33
+ "name": "denies writing into storage/framework",
34
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "storage/framework/views/cached.php" } },
35
+ "expect_exit": 2,
36
+ "expect_stdout_contains": "framework caches"
37
+ },
38
+ {
39
+ "name": "denies writing into bootstrap/cache",
40
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "bootstrap/cache/config.php" } },
41
+ "expect_exit": 2,
42
+ "expect_stdout_contains": "framework caches"
43
+ },
44
+ {
45
+ "name": "denies writing into storage/logs",
46
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "storage/logs/laravel.log" } },
47
+ "expect_exit": 2,
48
+ "expect_stdout_contains": "runtime logs"
49
+ },
50
+ {
51
+ "name": "denies writing into __pycache__",
52
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "app/__pycache__/module.cpython-311.pyc" } },
53
+ "expect_exit": 2,
54
+ "expect_stdout_contains": "__pycache__"
55
+ },
56
+ {
57
+ "name": "denies writing into .venv",
58
+ "input": { "tool_name": "Write", "tool_input": { "file_path": ".venv/lib/python3.11/site-packages/foo.py" } },
59
+ "expect_exit": 2,
60
+ "expect_stdout_contains": "virtual environment"
61
+ },
62
+ {
63
+ "name": "denies writing into .pytest_cache",
64
+ "input": { "tool_name": "Write", "tool_input": { "file_path": ".pytest_cache/v/cache/lastfailed" } },
65
+ "expect_exit": 2,
66
+ "expect_stdout_contains": "Python tool caches"
67
+ },
68
+ {
69
+ "name": "denies writing into coverage",
70
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "coverage/lcov.info" } },
71
+ "expect_exit": 2,
72
+ "expect_stdout_contains": "coverage"
73
+ },
74
+ {
75
+ "name": "denies writing a .DS_Store",
76
+ "input": { "tool_name": "Write", "tool_input": { "file_path": ".DS_Store" } },
77
+ "expect_exit": 2,
78
+ "expect_stdout_contains": ".DS_Store"
79
+ },
80
+ {
81
+ "name": "denies writing a .so binary",
82
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "lib/native.so" } },
83
+ "expect_exit": 2,
84
+ "expect_stdout_contains": "binary files"
85
+ },
86
+ {
87
+ "name": "denies writing a .exe",
88
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "bin/tool.exe" } },
89
+ "expect_exit": 2,
90
+ "expect_stdout_contains": "binary files"
91
+ },
92
+ {
93
+ "name": "denies writing a .zip archive",
94
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "release/bundle.zip" } },
95
+ "expect_exit": 2,
96
+ "expect_stdout_contains": "archive"
97
+ },
98
+ {
99
+ "name": "denies writing a .phar",
100
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "tools/composer.phar" } },
101
+ "expect_exit": 2,
102
+ "expect_stdout_contains": "archive"
103
+ },
104
+ {
105
+ "name": "denies writing an mp4",
106
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "assets/intro.mp4" } },
107
+ "expect_exit": 2,
108
+ "expect_stdout_contains": "media files"
109
+ },
110
+ {
111
+ "name": "denies writing a .pyc",
112
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "app/module.pyc" } },
113
+ "expect_exit": 2,
114
+ "expect_stdout_contains": "bytecode"
115
+ },
116
+ {
117
+ "name": "allows writing a regular Python file",
118
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "app/domains/service.py" } },
119
+ "expect_exit": 0,
120
+ "expect_stdout_not_contains": "permissionDecision"
121
+ },
122
+ {
123
+ "name": "allows writing a regular PHP file",
124
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "app/Cerebro/Domains/Services/DomainService.php" } },
125
+ "expect_exit": 0,
126
+ "expect_stdout_not_contains": "permissionDecision"
127
+ },
128
+ {
129
+ "name": "allows writing a Vue file in resources",
130
+ "input": { "tool_name": "Write", "tool_input": { "file_path": "resources/js/pages/domains/Index.vue" } },
131
+ "expect_exit": 0,
132
+ "expect_stdout_not_contains": "permissionDecision"
133
+ },
134
+ {
135
+ "name": "allows writing a .gitignore",
136
+ "input": { "tool_name": "Write", "tool_input": { "file_path": ".gitignore" } },
137
+ "expect_exit": 0,
138
+ "expect_stdout_not_contains": "permissionDecision"
139
+ },
140
+ {
141
+ "name": "no-op when file_path is missing",
142
+ "input": { "tool_name": "Bash", "tool_input": { "command": "ls" } },
143
+ "expect_exit": 0,
144
+ "expect_stdout_not_contains": "permissionDecision"
145
+ }
146
+ ]
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env bash
2
+ # Runs all hook test cases and reports pass/fail.
3
+ #
4
+ # Usage:
5
+ # .claude/hooks/tests/run.sh # run all hooks
6
+ # .claude/hooks/tests/run.sh protect-files # run one hook
7
+ #
8
+ # Case files live in .claude/hooks/tests/cases/<hook>.cases.json.
9
+ # Each case is an object with: name, input, expect_exit, expect_stdout_contains,
10
+ # expect_stdout_not_contains. See README.md for the full schema.
11
+
12
+ set -uo pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ HOOKS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
16
+ CASES_DIR="$SCRIPT_DIR/cases"
17
+
18
+ # Hermetic run: cases assert default behavior, so neutralise the env knobs that
19
+ # a maintainer's session may set (Forge's own dev repo exports both). Without
20
+ # this, FORGE_ALLOW_HOOK_EDITS=1 lifts protect-files' hook/settings guards and an
21
+ # empty CLAUDE_PROTECTED_BRANCHES disables the push checks — turning real cases red.
22
+ unset FORGE_ALLOW_HOOK_EDITS
23
+ unset CLAUDE_PROTECTED_BRANCHES
24
+
25
+ if ! command -v jq >/dev/null 2>&1; then
26
+ printf 'jq is required to run hook tests. Install with: brew install jq (macOS) or apt install jq (Linux).\n' >&2
27
+ exit 1
28
+ fi
29
+
30
+ # Optional single-hook filter.
31
+ FILTER="${1:-}"
32
+
33
+ PASSED=0
34
+ FAILED=0
35
+ SKIPPED=0
36
+ FAILURES=()
37
+
38
+ run_case() {
39
+ local hook_script="$1" case_json="$2"
40
+ local name input expect_exit expect_contains expect_not_contains
41
+ name=$(printf '%s' "$case_json" | jq -r '.name')
42
+ input=$(printf '%s' "$case_json" | jq -c '.input')
43
+ expect_exit=$(printf '%s' "$case_json" | jq -r '.expect_exit // 0')
44
+ expect_contains=$(printf '%s' "$case_json" | jq -r '.expect_stdout_contains // ""')
45
+ expect_not_contains=$(printf '%s' "$case_json" | jq -r '.expect_stdout_not_contains // ""')
46
+
47
+ local output rc
48
+ output=$(printf '%s' "$input" | "$hook_script" 2>&1)
49
+ rc=$?
50
+
51
+ local reasons=()
52
+ if [ "$rc" -ne "$expect_exit" ]; then
53
+ reasons+=("exit $rc, expected $expect_exit")
54
+ fi
55
+ if [ -n "$expect_contains" ] && ! printf '%s' "$output" | grep -qF -- "$expect_contains"; then
56
+ reasons+=("stdout missing: $expect_contains")
57
+ fi
58
+ if [ -n "$expect_not_contains" ] && printf '%s' "$output" | grep -qF -- "$expect_not_contains"; then
59
+ reasons+=("stdout should not contain: $expect_not_contains")
60
+ fi
61
+
62
+ if [ "${#reasons[@]}" -eq 0 ]; then
63
+ printf ' [PASS] %s\n' "$name"
64
+ PASSED=$((PASSED + 1))
65
+ else
66
+ printf ' [FAIL] %s\n' "$name"
67
+ for reason in "${reasons[@]}"; do
68
+ printf ' %s\n' "$reason"
69
+ done
70
+ if [ -n "$output" ]; then
71
+ printf ' output: %s\n' "$output" | head -3
72
+ fi
73
+ FAILED=$((FAILED + 1))
74
+ FAILURES+=("$name")
75
+ fi
76
+ }
77
+
78
+ for case_file in "$CASES_DIR"/*.cases.json; do
79
+ [ -f "$case_file" ] || continue
80
+
81
+ hook_name=$(basename "$case_file" .cases.json)
82
+
83
+ if [ -n "$FILTER" ] && [ "$hook_name" != "$FILTER" ]; then
84
+ continue
85
+ fi
86
+
87
+ hook_script="$HOOKS_DIR/$hook_name.sh"
88
+
89
+ if [ ! -f "$hook_script" ]; then
90
+ printf '[SKIP] %s — hook script not found at %s\n' "$hook_name" "$hook_script"
91
+ SKIPPED=$((SKIPPED + 1))
92
+ continue
93
+ fi
94
+
95
+ if [ ! -x "$hook_script" ]; then
96
+ printf '[SKIP] %s — hook script not executable (run: chmod +x %s)\n' "$hook_name" "$hook_script"
97
+ SKIPPED=$((SKIPPED + 1))
98
+ continue
99
+ fi
100
+
101
+ printf '\n%s\n' "$hook_name"
102
+
103
+ count=$(jq -r 'length' "$case_file")
104
+ for i in $(seq 0 $((count - 1))); do
105
+ case_json=$(jq -c ".[$i]" "$case_file")
106
+ run_case "$hook_script" "$case_json"
107
+ done
108
+ done
109
+
110
+ printf '\n---\n%d passed, %d failed, %d skipped\n' "$PASSED" "$FAILED" "$SKIPPED"
111
+
112
+ if [ "$FAILED" -gt 0 ]; then
113
+ printf '\nFailed cases:\n'
114
+ for f in "${FAILURES[@]}"; do
115
+ printf ' - %s\n' "$f"
116
+ done
117
+ exit 1
118
+ fi
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # Forge safety guardrail — blocks writes to build artifacts, binaries, and
3
+ # dependency directories. PreToolUse hook for Edit|Write. Exit 2 = block.
4
+
5
+ set -uo pipefail
6
+
7
+ if ! command -v jq >/dev/null 2>&1; then
8
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"jq is required for file protection hooks but is not installed.\"}}"
9
+ exit 2
10
+ fi
11
+
12
+ INPUT=$(cat)
13
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
14
+ [ -z "$FILE_PATH" ] && exit 0
15
+
16
+ # Dependency, build, and cache directories across common stacks.
17
+ REASON=""
18
+ case "$FILE_PATH" in
19
+ # Node / JS / TS
20
+ node_modules/*|*/node_modules/*)
21
+ REASON="Cannot write into node_modules/ — install dependencies via the package manager instead." ;;
22
+ dist/*|*/dist/*|build/*|*/build/*|.next/*|*/.next/*|.nuxt/*|*/.nuxt/*|.output/*|*/.output/*)
23
+ REASON="Cannot write into build output directories — these are generated by the build process." ;;
24
+ # PHP / Laravel
25
+ vendor/*|*/vendor/*)
26
+ REASON="Cannot write into vendor/ — use composer instead." ;;
27
+ storage/framework/*|*/storage/framework/*|bootstrap/cache/*|*/bootstrap/cache/*)
28
+ REASON="Cannot write into Laravel framework caches — these are rebuilt by artisan." ;;
29
+ storage/logs/*|*/storage/logs/*)
30
+ REASON="Cannot write into storage/logs/ — these are runtime logs." ;;
31
+ # Python
32
+ __pycache__/*|*/__pycache__/*)
33
+ REASON="Cannot write into __pycache__/ — these are generated by Python." ;;
34
+ .venv/*|*/.venv/*|venv/*|*/venv/*)
35
+ REASON="Cannot write into virtual environment directories." ;;
36
+ .pytest_cache/*|*/.pytest_cache/*|.ruff_cache/*|*/.ruff_cache/*|.mypy_cache/*|*/.mypy_cache/*|.tox/*|*/.tox/*)
37
+ REASON="Cannot write into Python tool caches (pytest/ruff/mypy/tox) — these are regenerated automatically." ;;
38
+ # Go / Rust
39
+ target/debug/*|*/target/debug/*|target/release/*|*/target/release/*)
40
+ REASON="Cannot write into Rust target/ build output." ;;
41
+ # Generic
42
+ coverage/*|*/coverage/*|htmlcov/*|*/htmlcov/*|.nyc_output/*|*/.nyc_output/*)
43
+ REASON="Cannot write into coverage reports — these are regenerated by the test runner." ;;
44
+ .DS_Store|*/.DS_Store)
45
+ REASON="Cannot write .DS_Store — macOS metadata files should not be committed." ;;
46
+ esac
47
+
48
+ if [ -n "$REASON" ]; then
49
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"$REASON\"}}"
50
+ exit 2
51
+ fi
52
+
53
+ # Block binary and archive file extensions.
54
+ BASENAME=$(basename "$FILE_PATH")
55
+ case "$BASENAME" in
56
+ *.wasm|*.so|*.dylib|*.dll|*.exe|*.o|*.a)
57
+ REASON="Cannot write binary files — these should be compiled, not hand-written." ;;
58
+ *.zip|*.tar|*.tar.gz|*.tar.bz2|*.tgz|*.rar|*.7z|*.phar)
59
+ REASON="Cannot write archive/bundle files." ;;
60
+ *.mp4|*.mov|*.avi|*.mkv|*.mp3|*.wav|*.flac)
61
+ REASON="Cannot write media files — add these manually outside the agent." ;;
62
+ *.pyc|*.pyo|*.class)
63
+ REASON="Cannot write compiled bytecode files." ;;
64
+ esac
65
+
66
+ if [ -n "$REASON" ]; then
67
+ echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"$REASON\"}}"
68
+ exit 2
69
+ fi
70
+
71
+ exit 0
@@ -0,0 +1,63 @@
1
+ # Rules
2
+
3
+ Rules are short, ambient reminders that load passively based on which files are being edited. They are the lightweight counterpart to skills — cheap, file-path-triggered convention reminders that cost no context until you touch the relevant code. See [ADR-018](../../docs/decisions/ADR-018-lightweight-policy-layer.md).
4
+
5
+ ## The three weight tiers
6
+
7
+ Forge governs agent behaviour at three different weights. Rules fill the gap between the always-loaded heavy constitution and the opt-in heavy skills.
8
+
9
+ | Tier | Where | Weight | Loads when |
10
+ |---|---|---|---|
11
+ | **Constitution** | `.forge/constitution.md` | Heavy, project-wide hard rules | Always loaded — architectural **gates** that must pass |
12
+ | **Rules** | `.claude/rules/` | Lightweight ambient guidance | Always-on (no frontmatter) **or** path-scoped (auto-attaches only when editing matching files) |
13
+ | **Skills** | `.claude/skills/` | Heavy, full checklists/decision trees | Opt-in — loaded when a task description matches the skill |
14
+
15
+ - **Constitution** = the non-negotiable architectural gates that gate shipping. Heavy, always paid, project-specific.
16
+ - **Rules** = "don't add a phantom dependency", "re-read conventions on long sessions" — one-line bullets that fire ambiently. Path-scoped rules cost **zero context** until you touch a file matching their glob.
17
+ - **Skills** = "how we run a security review" (50–200 lines, invoked deliberately by task description).
18
+
19
+ The gap rules fill: the constitution is too heavy and too project-architectural for everyday convention reminders, and skills are opt-in so they miss ambient failure modes. Rules are the cheap, file-path-triggered middle layer.
20
+
21
+ Think of it this way:
22
+
23
+ - Constitution = "every API route enforces tenant isolation" (a gate, always checked)
24
+ - Skill = "how we run a security review" (50–200 lines, invoked deliberately)
25
+ - Rule = "don't call an API before grepping that it exists" (1-line bullet, fires ambiently)
26
+
27
+ If a rule grows past 25 lines, it is a skill in disguise. Split it or promote it.
28
+
29
+ ## File format
30
+
31
+ Every rule is a markdown file. Two modes:
32
+
33
+ **Always-on:** no frontmatter. Claude Code loads rules without a `paths:` field unconditionally (same priority as `.claude/CLAUDE.md`). Use for universal discipline (agent behaviour).
34
+
35
+ **Path-scoped:** YAML frontmatter with a `paths:` array of globs.
36
+ ```yaml
37
+ ---
38
+ paths:
39
+ - "src/**/*.ts"
40
+ - "app/**/Http/**"
41
+ ---
42
+ ```
43
+ Loaded only when Claude is reading or editing a file whose path matches any glob. Use for stack-specific or layer-specific guidance.
44
+
45
+ Do not use `alwaysApply: true`. That is Cursor syntax; Claude Code ignores it. Absence of `paths:` is the always-on signal.
46
+
47
+ ## Stack-specific rules ship opt-in
48
+
49
+ Forge core is **stack-agnostic**, so core ships only the always-on `agent-discipline.md`. Stack-specific path-scoped rules (Laravel, Vue, Python, etc.) live in the opt-in convention packs under `experimental/conventions/` — install them when your project's stack matches, and they auto-attach via their `paths:` globs. Keeping them out of core is what keeps core portable across any stack.
50
+
51
+ ## Current rules
52
+
53
+ | File | Scope | Topic |
54
+ |---|---|---|
55
+ | `agent-discipline.md` | always | Scope creep, phantom deps, hallucinated APIs, silent assumptions |
56
+
57
+ ## Authoring rules
58
+
59
+ - Keep each rule under ~20 lines of actual guidance.
60
+ - Lead with the imperative ("Never use ...", "Always declare ..."), not justification.
61
+ - No code blocks unless the exact syntax is the point.
62
+ - One file per concern. Do not bundle unrelated rules into a single file just because they share a path.
63
+ - If a rule needs a long explanation, it belongs in a skill. Rules state the rule; skills teach it. Past 25 lines, it's a skill in disguise.
@@ -0,0 +1,14 @@
1
+ # Agent Discipline
2
+
3
+ Always-on rule. These are the failure modes AI agents fall into most often. They apply to every session, regardless of stack. This restates the everyday floor; the executor's Deviation Rules (Rule 4 — STOP and ask before a new DB table, service layer, or library swap) in `.claude/agents/executor.md` are the hard stops.
4
+
5
+ - **Scope creep.** Do not modify files outside the stated scope. If a change requires touching something adjacent, ask first.
6
+ - **No phantom dependencies.** Do not add packages (`npm install`, `pip install`, `composer require`, `uv add`, etc.) without explicit approval. Name the package and version you need and wait. (A library swap is a Rule 4 stop.)
7
+ - **No over-engineering.** No abstractions, patterns, factories, registries, or base classes unless the plan calls for them. One concrete implementation does not need an interface.
8
+ - **No happy-path-only code.** Handle the unhappy paths the plan named. If the plan didn't name them, list what could go wrong before coding.
9
+ - **No hallucinated APIs.** Before calling a function or method, verify it exists with grep or by reading the source. If unsure, say so.
10
+ - **No silent assumptions.** Surface every non-obvious decision: "I assumed X because Y — confirm or redirect."
11
+ - **No "while I was here" extras.** No added logging, metrics, renames, or cleanup unless the task requires them.
12
+ - **Ask when unclear.** One good clarifying question beats an hour of wrong work.
13
+ - **Re-read conventions when the session gets long.** Style drifts over multi-hour sessions. The constitution and the relevant skill are the source of truth.
14
+ - **Test what you wrote.** Don't declare done without running the specific test or equivalent verification for the changed behaviour.