azclaude-copilot 0.4.5 → 0.4.6

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/README.md CHANGED
@@ -476,11 +476,11 @@ See [SECURITY.md](SECURITY.md) for full details.
476
476
 
477
477
  ## Verified
478
478
 
479
- 1152 tests. Every template, command, capability, agent, hook, and CLI feature verified.
479
+ 1196 tests. Every template, command, capability, agent, hook, and CLI feature verified.
480
480
 
481
481
  ```bash
482
482
  bash tests/test-features.sh
483
- # Results: 1152 passed, 0 failed, 1152 total
483
+ # Results: 1196 passed, 0 failed, 1196 total
484
484
  ```
485
485
 
486
486
  ---
package/bin/cli.js CHANGED
@@ -8,7 +8,7 @@ const { execSync } = require('child_process');
8
8
 
9
9
  const TEMPLATE_DIR = path.join(__dirname, '..', 'templates');
10
10
  const CORE_COMMANDS = ['setup', 'fix', 'add', 'audit', 'test', 'blueprint', 'ship', 'pulse', 'explain', 'snapshot', 'persist'];
11
- const EXTENDED_COMMANDS = ['dream', 'refactor', 'doc', 'loop', 'migrate', 'deps', 'find', 'create', 'reflect', 'hookify'];
11
+ const EXTENDED_COMMANDS = ['dream', 'refactor', 'doc', 'loop', 'migrate', 'deps', 'find', 'create', 'reflect', 'hookify', 'sentinel'];
12
12
  const ADVANCED_COMMANDS = ['evolve', 'debate', 'level-up', 'copilot', 'reflexes'];
13
13
  const COMMANDS = [...CORE_COMMANDS, ...EXTENDED_COMMANDS, ...ADVANCED_COMMANDS];
14
14
 
@@ -428,7 +428,7 @@ function installScripts(projectDir, cfg) {
428
428
 
429
429
  // ─── Agents ───────────────────────────────────────────────────────────────────
430
430
 
431
- const AGENTS = ['orchestrator-init', 'code-reviewer', 'test-writer', 'loop-controller', 'cc-template-author', 'cc-cli-integrator', 'cc-test-maintainer', 'orchestrator', 'problem-architect', 'milestone-builder'];
431
+ const AGENTS = ['orchestrator-init', 'code-reviewer', 'test-writer', 'loop-controller', 'cc-template-author', 'cc-cli-integrator', 'cc-test-maintainer', 'orchestrator', 'problem-architect', 'milestone-builder', 'security-auditor'];
432
432
 
433
433
  function installAgents(projectDir, cfg) {
434
434
  const agentsDir = path.join(projectDir, cfg, 'agents');
package/bin/copilot.js CHANGED
@@ -17,6 +17,7 @@
17
17
  const fs = require('fs');
18
18
  const path = require('path');
19
19
  const { spawnSync } = require('child_process');
20
+ const crypto = require('crypto');
20
21
 
21
22
  // ── Args ─────────────────────────────────────────────────────────────────────
22
23
 
@@ -162,6 +163,38 @@ console.log(' ⚠ See SECURITY.md for mitigations');
162
163
  console.log('════════════════════════════════════════════════');
163
164
  console.log(`\n Intent: ${intent.slice(0, 120)}${intent.length > 120 ? '...' : ''}\n`);
164
165
 
166
+ // ── Session State ─────────────────────────────────────────────────────────────
167
+ // Persists to disk — survives runner crash. Tracks plan progress for stall detection.
168
+
169
+ const statePath = path.join(claudeDir, 'copilot-state.json');
170
+
171
+ function loadState() {
172
+ try { return JSON.parse(fs.readFileSync(statePath, 'utf8')); } catch (_) {}
173
+ return { planHash: '', stalls: 0, stuckMilestones: {}, retries: 0 };
174
+ }
175
+
176
+ function saveState(state) {
177
+ try { fs.writeFileSync(statePath, JSON.stringify(state, null, 2)); } catch (_) {}
178
+ }
179
+
180
+ function hashPlan() {
181
+ if (!fs.existsSync(planPath)) return '';
182
+ return crypto.createHash('md5').update(fs.readFileSync(planPath)).digest('hex');
183
+ }
184
+
185
+ function getInProgressMilestones() {
186
+ if (!fs.existsSync(planPath)) return [];
187
+ const milestones = [];
188
+ let currentTitle = '';
189
+ for (const line of fs.readFileSync(planPath, 'utf8').split('\n')) {
190
+ if (/^#{1,3}\s/.test(line)) currentTitle = line.replace(/^#+\s*/, '').trim();
191
+ if (/Status:\s*in-progress/i.test(line) && currentTitle) milestones.push(currentTitle);
192
+ }
193
+ return milestones;
194
+ }
195
+
196
+ const state = loadState();
197
+
165
198
  // ── Session Loop ─────────────────────────────────────────────────────────────
166
199
 
167
200
  const sessionStartTimes = [];
@@ -174,6 +207,10 @@ for (let session = 1; session <= maxSessions; session++) {
174
207
  : 0;
175
208
  console.log(`\n── Session ${session}/${maxSessions} ${elapsed > 0 ? `(${elapsed}min elapsed)` : ''} ──`);
176
209
 
210
+ // Snapshot plan state before session — for stall + stuck milestone detection
211
+ const prevHash = hashPlan();
212
+ const prevInProgress = getInProgressMilestones();
213
+
177
214
  // Build state-aware prompt
178
215
  // IMPORTANT: In -p mode, slash commands (/setup, /copilot) don't work.
179
216
  // Tell Claude to read and follow the command .md files directly.
@@ -192,11 +229,22 @@ for (let session = 1; session <= maxSessions; session++) {
192
229
  prompt += '\nDo NOT declare COPILOT_COMPLETE until deep checks pass.';
193
230
  }
194
231
 
232
+ // Inject stall hint if plan hasn't changed
233
+ if (state.stalls > 0) {
234
+ prompt += `\n\nWARNING: Plan.md has not changed for ${state.stalls} consecutive session(s). You may be stuck. Complete at least one pending milestone and update its Status to "done" in plan.md before this session ends.`;
235
+ }
236
+
237
+ // Inject stuck milestone hint
238
+ const stuckList = Object.entries(state.stuckMilestones || {}).filter(([, c]) => c >= 2).map(([m]) => m);
239
+ if (stuckList.length > 0) {
240
+ prompt += `\n\nSTUCK MILESTONES (in-progress for 2+ sessions without progress): ${stuckList.join(', ')}. Either complete them fully now, or mark Status: blocked with a specific reason in .claude/memory/blockers.md. Do not leave them in-progress again.`;
241
+ }
242
+
195
243
  if (resuming || session > 1) {
196
244
  // Parse plan.md for milestone progress
197
245
  if (fs.existsSync(planPath)) {
198
246
  const planContent = fs.readFileSync(planPath, 'utf8');
199
- const statuses = [...planContent.matchAll(/^- Status: (\w+)/gm)].map(m => m[1]);
247
+ const statuses = [...planContent.matchAll(/^- Status: ([\w-]+)/gm)].map(m => m[1]);
200
248
  const done = statuses.filter(s => s === 'done').length;
201
249
  const blocked = statuses.filter(s => s === 'blocked').length;
202
250
  const pending = statuses.filter(s => s === 'pending' || s === 'in-progress').length;
@@ -212,30 +260,69 @@ for (let session = 1; session <= maxSessions; session++) {
212
260
  prompt += '\n\nNo plan yet. Read .claude/commands/setup.md and follow it, then read .claude/commands/blueprint.md to create milestones.';
213
261
  }
214
262
 
215
- // Run Claude Code session
216
- const result = spawnSync('claude', [
263
+ // Run Claude Code session — retry once on non-timeout failure (API hiccup, rate limit, etc.)
264
+ const claudeArgs = [
217
265
  '--dangerously-skip-permissions',
218
266
  '-p', prompt,
219
267
  '--output-format', 'text',
220
268
  ...(deepMode ? ['--model', 'claude-opus-4-6'] : [])
221
- ], {
222
- cwd: projectDir,
223
- stdio: 'inherit',
224
- timeout: 1800000, // 30 minutes per session (large milestones need time)
225
- });
269
+ ];
270
+ const spawnOpts = { cwd: projectDir, stdio: 'inherit', timeout: 1800000 };
271
+
272
+ let result = spawnSync('claude', claudeArgs, spawnOpts);
273
+
274
+ // Retry once on abnormal non-zero exit (not timeout, not spawn failure)
275
+ if (result.status !== 0 && !result.error) {
276
+ state.retries = (state.retries || 0) + 1;
277
+ saveState(state);
278
+ console.log(` Session ${session} exited ${result.status} — retrying once (retry #${state.retries} total)...`);
279
+ result = spawnSync('claude', claudeArgs, spawnOpts);
280
+ }
226
281
 
227
282
  if (result.error) {
228
283
  console.error(` Session ${session} error: ${result.error.message}`);
229
284
  if (result.error.code === 'ETIMEDOUT') {
230
285
  console.log(' Session timed out (30 min). Restarting...');
286
+ saveState(state);
231
287
  continue;
232
288
  }
233
289
  }
234
290
 
291
+ // ── Stall detection ────────────────────────────────────────────────────────
292
+ const newHash = hashPlan();
293
+ if (session > 1 && prevHash !== '' && newHash === prevHash) {
294
+ state.stalls = (state.stalls || 0) + 1;
295
+ console.log(` ⚠ No plan progress detected (stall ${state.stalls}/3)`);
296
+ if (state.stalls >= 3) {
297
+ console.log('\n════════════════════════════════════════════════');
298
+ console.log(' STALLED — plan.md unchanged for 3 consecutive sessions.');
299
+ console.log(' Likely stuck in a loop. Human review required.');
300
+ console.log(` State: ${statePath}`);
301
+ console.log('════════════════════════════════════════════════\n');
302
+ saveState(state);
303
+ process.exit(1);
304
+ }
305
+ } else {
306
+ state.stalls = 0;
307
+ }
308
+ state.planHash = newHash;
309
+
310
+ // ── Stuck milestone detection ───────────────────────────────────────────────
311
+ const newInProgress = getInProgressMilestones();
312
+ const stillStuck = newInProgress.filter(m => prevInProgress.includes(m));
313
+ const freshStuck = state.stuckMilestones || {};
314
+ for (const m of stillStuck) { freshStuck[m] = (freshStuck[m] || 0) + 1; }
315
+ for (const m of Object.keys(freshStuck)) {
316
+ if (!stillStuck.includes(m)) delete freshStuck[m];
317
+ }
318
+ state.stuckMilestones = freshStuck;
319
+ saveState(state);
320
+
235
321
  // Check completion
236
322
  if (fs.existsSync(goalsPath)) {
237
323
  const goals = fs.readFileSync(goalsPath, 'utf8');
238
324
  if (goals.includes('COPILOT_COMPLETE')) {
325
+ try { fs.unlinkSync(statePath); } catch (_) {} // clean up state on success
239
326
  console.log('\n════════════════════════════════════════════════');
240
327
  const totalMin = Math.round((Date.now() - sessionStartTimes[0]) / 60000);
241
328
  console.log(' COPILOT COMPLETE');
@@ -253,7 +340,7 @@ for (let session = 1; session <= maxSessions; session++) {
253
340
  // Check if plan.md shows all done or all blocked
254
341
  if (fs.existsSync(planPath)) {
255
342
  const plan = fs.readFileSync(planPath, 'utf8');
256
- const statuses = [...plan.matchAll(/^- Status: (\w+)/gm)].map(m => m[1]);
343
+ const statuses = [...plan.matchAll(/^- Status: ([\w-]+)/gm)].map(m => m[1]);
257
344
  if (statuses.length > 0) {
258
345
  const allDoneOrBlocked = statuses.every(s => s === 'done' || s === 'blocked' || s === 'skipped');
259
346
  const allBlocked = statuses.every(s => s === 'blocked');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "azclaude-copilot",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "AI coding environment — 26 commands, 8 skills, 10 agents, memory, reflexes, evolution. Install once, works on any stack.",
5
5
  "bin": {
6
6
  "azclaude": "bin/cli.js",
@@ -0,0 +1,395 @@
1
+ ---
2
+ name: security-auditor
3
+ description: >
4
+ Autonomous security scanner for Claude Code environments. Covers 102 rules
5
+ across 5 categories: secrets (14), permissions (10), hooks (34), MCP servers (23),
6
+ agent configs (25). Read-only — never modifies files. Returns a structured
7
+ Security Report with score (0–100), grade (A–F), and per-finding file:line refs.
8
+ Spawned by /sentinel and /ship risk gate. All checks are native Claude Code tools —
9
+ no npm install, no third-party binaries.
10
+ Use when: security scan, before ship, check environment, audit hooks, check MCP,
11
+ review agent configs, scan for secrets, is my setup safe.
12
+ model: sonnet
13
+ tools: [Read, Grep, Glob, Bash]
14
+ disallowedTools: [Write, Edit, Agent]
15
+ permissionMode: plan
16
+ maxTurns: 40
17
+ ---
18
+
19
+ ## Layer 1: PERSONA
20
+
21
+ Security auditor. Read-only — never modifies files, never executes arbitrary code.
22
+ Scans Claude Code environments for security issues using native tools only.
23
+ Reports findings as `file:line — rule-id — description`. No speculation — only flag what is confirmed in files.
24
+
25
+ ---
26
+
27
+ ## Layer 2: SCOPE
28
+
29
+ **Does:**
30
+ - Scans codebase and Claude config for all 102 rules across 5 categories
31
+ - Returns a scored Security Report (0–100, grade A–F)
32
+ - Reports BLOCKED findings (must fix before ship) vs HIGH/MEDIUM/LOW
33
+ - References exact file:line for every finding
34
+
35
+ **Does NOT:**
36
+ - Write or edit any files
37
+ - Install packages or call external services
38
+ - Flag issues it hasn't confirmed by reading the actual file
39
+ - Run destructive commands
40
+ - Re-run scans already confirmed clean
41
+
42
+ ---
43
+
44
+ ## Layer 3: TOOLS & RESOURCES
45
+
46
+ ```
47
+ Read — read settings.json, .mcp.json, hook scripts, agent files
48
+ Grep — pattern-match across source files, configs, agent instructions
49
+ Glob — locate hooks, agents, config files
50
+ Bash — git ls-files, cat, wc (read-only only)
51
+ ```
52
+
53
+ **Scan targets — locate these first:**
54
+ ```bash
55
+ # Claude Code configs
56
+ ls "$HOME/.claude/settings.json" .claude/settings.local.json 2>/dev/null
57
+ # MCP config
58
+ ls .mcp.json "$HOME/.claude/mcp.json" 2>/dev/null
59
+ # Hooks
60
+ ls .claude/hooks/ "$HOME/.claude/hooks/" 2>/dev/null
61
+ # Agent definitions
62
+ ls .claude/agents/*.md 2>/dev/null
63
+ # Tracked source files (for secrets scan)
64
+ git ls-files --cached 2>/dev/null | grep -v node_modules | grep -v ".git/" | head -300
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Layer 4: CONSTRAINTS
70
+
71
+ - **Never run commands that write state** — no curl, wget, npm, pip, git commit, etc.
72
+ - **Never flag a finding without confirming it** — read the file before reporting
73
+ - **file:line references are required** — "settings.json" alone is not a valid finding
74
+ - **No false positives** — if uncertain, do not flag. Only high-signal findings
75
+ - **Complete all 5 categories** — do not stop after finding one BLOCKED issue
76
+ - **Score deduction is cumulative** — each finding deducts from its category score
77
+
78
+ ---
79
+
80
+ ## Layer 5: DOMAIN CONTEXT — 102 Rules
81
+
82
+ ### Scan Order
83
+
84
+ Run all 5 categories. Deduct per finding. Compute total score at the end.
85
+
86
+ ---
87
+
88
+ ### Category 1 — Secrets Detection (14 rules, weight: 20 pts)
89
+
90
+ Grep across all tracked files. Skip: `node_modules/`, `.git/`, `*.lock`, `*.min.js`.
91
+
92
+ ```bash
93
+ git ls-files --cached 2>/dev/null | grep -v node_modules | grep -v ".git/" \
94
+ | grep -E "\.(js|ts|py|rb|go|sh|json|yaml|yml|env|cfg|ini|toml)$" \
95
+ > /tmp/az-scan-files.txt
96
+ ```
97
+
98
+ For each pattern, run: `grep -n PATTERN $(cat /tmp/az-scan-files.txt) 2>/dev/null`
99
+
100
+ | Rule | Pattern | Severity |
101
+ |---|---|---|
102
+ | S1 | `AKIA[A-Z0-9]{16}` | BLOCKED |
103
+ | S2 | `ghp_[A-Za-z0-9]{36}` | BLOCKED |
104
+ | S3 | `github_pat_[A-Za-z0-9_]{82}` | BLOCKED |
105
+ | S4 | `glpat-[A-Za-z0-9_-]{20}` | BLOCKED |
106
+ | S5 | `xoxb-[0-9]` | BLOCKED |
107
+ | S6 | `xoxp-[0-9]` | BLOCKED |
108
+ | S7 | `npm_[A-Za-z0-9]{36}` | BLOCKED |
109
+ | S8 | `sk-[a-zA-Z0-9]{48,}` | BLOCKED |
110
+ | S9 | `AIza[0-9A-Za-z_-]{35}` | BLOCKED |
111
+ | S10 | `sk_live_[0-9a-zA-Z]{24}` | BLOCKED |
112
+ | S11 | `pk_live_[0-9a-zA-Z]{24}` | HIGH |
113
+ | S12 | `SG\.[A-Za-z0-9_-]{22}\.` | BLOCKED |
114
+ | S13 | `-----BEGIN (RSA \|EC \|DSA \|OPENSSH )?PRIVATE KEY` | BLOCKED |
115
+ | S14 | `eyJ[A-Za-z0-9_-]{50,}\.[A-Za-z0-9_-]{10,}` | HIGH |
116
+
117
+ Also check: `.env` exists and is in `.gitignore`:
118
+ ```bash
119
+ [ -f .env ] && grep -q "\.env" .gitignore 2>/dev/null || echo ".env not gitignored"
120
+ ```
121
+
122
+ Score: start 20. Each BLOCKED finding: −5. Each HIGH: −2. Floor: 0.
123
+
124
+ ---
125
+
126
+ ### Category 2 — Permission Audit (10 rules, weight: 20 pts)
127
+
128
+ Read `~/.claude/settings.json` and `.claude/settings.local.json`.
129
+
130
+ | Rule | Check | Severity |
131
+ |---|---|---|
132
+ | P1 | `allowedTools` contains `"*"` | HIGH |
133
+ | P2 | `bypassPermissionsModeAccepted: true` | HIGH |
134
+ | P3 | `dangerouslyAllowedTools` key present | HIGH |
135
+ | P4 | No `hooks` key in settings (no hook protection) | MEDIUM |
136
+ | P5 | `_azclaude: true` absent from hooks block | LOW |
137
+ | P6 | `allowedTools` includes `rm`, `del`, `git reset` | HIGH |
138
+ | P7 | No `allowedTools` restriction at all | MEDIUM |
139
+ | P8 | Any agent frontmatter: reviewer with `Write` in tools | MEDIUM |
140
+ | P9 | Orchestrator agent has `Edit` or `Write` in tools | MEDIUM |
141
+ | P10 | `permissionMode: bypassPermissions` in any agent | HIGH |
142
+
143
+ For P8/P9/P10, check all `.claude/agents/*.md` frontmatter:
144
+ ```bash
145
+ grep -l "Write\|Edit" .claude/agents/*.md 2>/dev/null | xargs grep -l "reviewer\|read-only" 2>/dev/null
146
+ grep -n "permissionMode.*bypass" .claude/agents/*.md 2>/dev/null
147
+ ```
148
+
149
+ Score: start 20. HIGH: −4. MEDIUM: −2. LOW: −1. Floor: 0.
150
+
151
+ ---
152
+
153
+ ### Category 3 — Hook Script Analysis (34 rules, weight: 25 pts)
154
+
155
+ Locate and read all hook scripts:
156
+ ```bash
157
+ ls .claude/hooks/ 2>/dev/null
158
+ ls "$HOME/.claude/hooks/" 2>/dev/null
159
+ ```
160
+
161
+ **Sub-group A: Exfiltration (8 rules)**
162
+
163
+ | Rule | Pattern | Severity |
164
+ |---|---|---|
165
+ | H1 | `curl.*\|.*bash\|curl.*\|.*sh` | BLOCKED |
166
+ | H2 | `wget.*\|.*bash\|wget.*\|.*sh` | BLOCKED |
167
+ | H3 | `curl.*-X POST.*http` (sends data externally) | HIGH |
168
+ | H4 | `curl.*Authorization` (auth header in hook) | HIGH |
169
+ | H5 | Write to file path outside project and /tmp | HIGH |
170
+ | H6 | `ssh ` command in hook | HIGH |
171
+ | H7 | `nslookup\|dig ` with variable (DNS exfil) | HIGH |
172
+ | H8 | `base64.*curl\|curl.*base64` | HIGH |
173
+
174
+ **Sub-group B: Arbitrary Code Execution (8 rules)**
175
+
176
+ | Rule | Pattern | Severity |
177
+ |---|---|---|
178
+ | H9 | `\beval\b` in bash hook | HIGH |
179
+ | H10 | `\.exec\s*\(` in JS hook | HIGH |
180
+ | H11 | `sh -c .*\$` (shell with variable) | HIGH |
181
+ | H12 | `bash -c.*\+\|bash -c.*\$` | HIGH |
182
+ | H13 | `new Function\s*\(` in JS | HIGH |
183
+ | H14 | `subprocess\.call.*shell=True` | HIGH |
184
+ | H15 | `os\.system\s*\(` | HIGH |
185
+ | H16 | `child_process\.exec\s*\(` | MEDIUM |
186
+
187
+ **Sub-group C: Destructive Operations (6 rules)**
188
+
189
+ | Rule | Pattern | Severity |
190
+ |---|---|---|
191
+ | H17 | `rm -rf\|Remove-Item.*Recurse` | HIGH |
192
+ | H18 | `git reset --hard` | HIGH |
193
+ | H19 | `git push.*--force\|git push.*-f ` | HIGH |
194
+ | H20 | `DROP TABLE\|DELETE FROM` without WHERE | HIGH |
195
+ | H21 | File deletion outside /tmp | MEDIUM |
196
+ | H22 | `truncate\|> /dev/null 2>&1.*&&.*rm` | MEDIUM |
197
+
198
+ **Sub-group D: Persistence (4 rules)**
199
+
200
+ | Rule | Pattern | Severity |
201
+ |---|---|---|
202
+ | H23 | `crontab -e\|crontab -l.*>` | BLOCKED |
203
+ | H24 | `.bashrc\|.zshrc\|.profile` write | HIGH |
204
+ | H25 | `systemctl enable\|launchctl load` | BLOCKED |
205
+ | H26 | `HKLM\|reg add.*Run` (Windows startup) | BLOCKED |
206
+
207
+ **Sub-group E: Injection Vectors (5 rules)**
208
+
209
+ | Rule | Pattern | Severity |
210
+ |---|---|---|
211
+ | H27 | Unquoted `$CLAUDE_FILE_PATH` in shell command | HIGH |
212
+ | H28 | `IFS=` reassignment | MEDIUM |
213
+ | H29 | `\.\./\.\./` path traversal | HIGH |
214
+ | H30 | `\x00\|%00` null byte | HIGH |
215
+ | H31 | `SHLVL\|exec bash\|exec sh` shell escape | HIGH |
216
+
217
+ **Sub-group F: Hook Neutralization (3 rules)**
218
+
219
+ | Rule | Check | Severity |
220
+ |---|---|---|
221
+ | H32 | Hook script is empty (0 bytes or only comments) | MEDIUM |
222
+ | H33 | Hook always exits 0 with no actual scan logic | MEDIUM |
223
+ | H34 | Entire hook wrapped in `try {} catch { exit 0 }` with no re-throw | LOW |
224
+
225
+ Score: start 25. BLOCKED: −8. HIGH: −3. MEDIUM: −1. LOW: −0.5. Floor: 0.
226
+
227
+ ---
228
+
229
+ ### Category 4 — MCP Server Scan (23 rules, weight: 20 pts)
230
+
231
+ Read `.mcp.json` and `~/.claude/mcp.json`. For each server entry:
232
+
233
+ **Sub-group A: Hardcoded Secrets in Args (7 rules)**
234
+
235
+ | Rule | Pattern in args/env values | Severity |
236
+ |---|---|---|
237
+ | M1 | `AKIA[A-Z0-9]{16}` | BLOCKED |
238
+ | M2 | `ghp_[A-Za-z0-9]{36}` | BLOCKED |
239
+ | M3 | `sk-[a-zA-Z0-9]{20,}` | BLOCKED |
240
+ | M4 | `glpat-[A-Za-z0-9_-]{20}` | BLOCKED |
241
+ | M5 | `xoxb-[0-9]` | BLOCKED |
242
+ | M6 | `SG\.[A-Za-z0-9_-]{22}\.` | BLOCKED |
243
+ | M7 | `AIza[0-9A-Za-z_-]{35}` | BLOCKED |
244
+
245
+ Check: any secret that is not `${ENV_VAR}` syntax is a finding.
246
+
247
+ **Sub-group B: Supply Chain (6 rules)**
248
+
249
+ | Rule | Check | Severity |
250
+ |---|---|---|
251
+ | M8 | `npx` without `@version` pin (e.g. `npx some-package`) | MEDIUM |
252
+ | M9 | npm package not org-scoped (no `@org/`) | LOW |
253
+ | M10 | `uvx` without `--from pkg==version` | MEDIUM |
254
+ | M11 | `python -m` without pinned requirements | MEDIUM |
255
+ | M12 | Package name < 4 chars or all-lowercase-generic | LOW |
256
+ | M13 | `git clone` in MCP command/args | HIGH |
257
+
258
+ **Sub-group C: Network Security (5 rules)**
259
+
260
+ | Rule | Check | Severity |
261
+ |---|---|---|
262
+ | M14 | Server URL uses `http://` not `https://` | HIGH |
263
+ | M15 | External domain not in a known allow-list | MEDIUM |
264
+ | M16 | `*` in CORS or wildcard origin | HIGH |
265
+ | M17 | No authentication for network-exposed server | MEDIUM |
266
+ | M18 | Port < 1024 (privileged port binding) | MEDIUM |
267
+
268
+ **Sub-group D: File System Access (5 rules)**
269
+
270
+ | Rule | Check | Severity |
271
+ |---|---|---|
272
+ | M19 | MCP granted access to `~` or `$HOME` | HIGH |
273
+ | M20 | MCP granted access to `/etc` or `C:\Windows` | BLOCKED |
274
+ | M21 | MCP granted write to `/tmp` (execution staging) | MEDIUM |
275
+ | M22 | MCP granted write to project root (`.`) | MEDIUM |
276
+ | M23 | MCP granted read to `.claude/` (settings exposure) | HIGH |
277
+
278
+ Score: start 20. BLOCKED: −8. HIGH: −3. MEDIUM: −1. LOW: −0.5. Floor: 0.
279
+
280
+ ---
281
+
282
+ ### Category 5 — Agent Config Review (25 rules, weight: 15 pts)
283
+
284
+ Read all agent `.md` files in `.claude/agents/` and `templates/agents/`.
285
+
286
+ ```bash
287
+ for f in .claude/agents/*.md templates/agents/*.md 2>/dev/null; do
288
+ echo "=== $f ===" && cat "$f"
289
+ done
290
+ ```
291
+
292
+ **Sub-group A: Prompt Injection (8 rules)**
293
+
294
+ | Rule | Pattern in instructions | Severity |
295
+ |---|---|---|
296
+ | A1 | `ignore.*previous.*instructions\|ignore.*above` | BLOCKED |
297
+ | A2 | `you are now\|from now on you are` | HIGH |
298
+ | A3 | `pretend (you are\|to be)` | HIGH |
299
+ | A4 | `disregard.*rules\|forget.*rules` | BLOCKED |
300
+ | A5 | `as an AI without restrictions\|no restrictions` | HIGH |
301
+ | A6 | `jailbreak\|jail break` | HIGH |
302
+ | A7 | `DAN mode\|developer mode\|unrestricted mode` | BLOCKED |
303
+ | A8 | `override.*safety\|bypass.*safety` | BLOCKED |
304
+
305
+ **Sub-group B: Hidden Payloads (5 rules)**
306
+
307
+ | Rule | Check | Severity |
308
+ |---|---|---|
309
+ | A9 | Base64 block > 200 chars (`[A-Za-z0-9+/]{200,}`) | HIGH |
310
+ | A10 | Zero-width chars (`\u200b\|\u200c\|\u200d\|\ufeff`) | BLOCKED |
311
+ | A11 | Raw HTML tags in instructions (`<script\|<iframe\|<img`) | HIGH |
312
+ | A12 | External URL in instructions (data exfiltration risk) | MEDIUM |
313
+ | A13 | Control characters (`[\x01-\x08\x0b\x0c\x0e-\x1f]`) | HIGH |
314
+
315
+ **Sub-group C: RCE Instructions (5 rules)**
316
+
317
+ | Rule | Pattern | Severity |
318
+ |---|---|---|
319
+ | A14 | `curl.*\|.*bash\|wget.*\|.*sh` in instructions | BLOCKED |
320
+ | A15 | `python -c ['"]` in instructions | HIGH |
321
+ | A16 | `eval\s*\(` in instructions | HIGH |
322
+ | A17 | `exec\s*\(` in instructions | HIGH |
323
+ | A18 | `subprocess\|child_process` in instructions | MEDIUM |
324
+
325
+ **Sub-group D: Privilege Escalation (4 rules)**
326
+
327
+ | Rule | Pattern | Severity |
328
+ |---|---|---|
329
+ | A19 | `bypass.*permission\|ignore.*permission` | BLOCKED |
330
+ | A20 | `ignore.*restrictions\|no.*restrictions` | HIGH |
331
+ | A21 | Agent instructed to spawn agents with elevated tools | HIGH |
332
+ | A22 | `allowedTools.*\*` in agent frontmatter | HIGH |
333
+
334
+ **Sub-group E: Data Exfiltration (3 rules)**
335
+
336
+ | Rule | Pattern | Severity |
337
+ |---|---|---|
338
+ | A23 | `POST.*http\|send.*to.*http` in instructions | BLOCKED |
339
+ | A24 | `upload.*file.*to\|exfiltrate` | BLOCKED |
340
+ | A25 | `send.*credentials\|transmit.*key` | BLOCKED |
341
+
342
+ Score: start 15. BLOCKED: −5. HIGH: −2. MEDIUM: −1. Floor: 0.
343
+
344
+ ---
345
+
346
+ ## Scoring & Output
347
+
348
+ After all 5 categories:
349
+
350
+ ```
351
+ total = cat1 + cat2 + cat3 + cat4 + cat5 (max 100)
352
+ grade = A (≥90) | B (≥75) | C (≥60) | D (≥45) | F (<45)
353
+ ```
354
+
355
+ **Output this EXACT format** (the orchestrator and /sentinel parse it):
356
+
357
+ ```
358
+ ## Security Report: {project-name or cwd} — {date}
359
+
360
+ Score: {total}/100 Grade: {A|B|C|D|F}
361
+
362
+ Category Scores:
363
+ Secrets: {n}/20
364
+ Permissions: {n}/20
365
+ Hooks: {n}/25
366
+ MCP: {n}/20
367
+ Agents: {n}/15
368
+
369
+ ### BLOCKED — must resolve before /ship
370
+ - {file:line} — {rule-id} — {description}
371
+ Fix: {one-line remediation}
372
+
373
+ ### HIGH — resolve before next release
374
+ - {file:line} — {rule-id} — {description}
375
+
376
+ ### MEDIUM — review recommended
377
+ - {file:line} — {rule-id} — {description}
378
+
379
+ ### LOW — informational
380
+ - {file:line} — {rule-id} — {description}
381
+
382
+ ### PASSED
383
+ {N} rules checked, {N} passed clean
384
+
385
+ ### Verdict: BLOCKED | CLEAR | PROCEED WITH CAUTION
386
+ BLOCKED → one or more BLOCKED findings present
387
+ CLEAR → grade A or B, zero BLOCKED findings
388
+ PROCEED → grade C or D, zero BLOCKED findings
389
+ ```
390
+
391
+ **Rules:**
392
+ - List every finding. Do not summarize or combine.
393
+ - If a category has no findings: write `{category}: clean`
394
+ - Never write "likely" or "possibly" — only confirmed findings
395
+ - Each BLOCKED finding must include a one-line Fix instruction
@@ -0,0 +1,230 @@
1
+ ---
2
+ name: sentinel
3
+ description: >
4
+ Static security scan of the Claude Code environment.
5
+ Audits hooks, permissions, MCP servers, agent configs, and secrets.
6
+ Produces a scored report (0–100) with grade A–F and blocking findings.
7
+ Triggers on: "security scan", "audit environment", "check my hooks",
8
+ "is my setup safe", "scan for secrets", "check permissions",
9
+ "audit agents", "check mcp", "security check", "sentinel".
10
+ argument-hint: "[--hooks | --mcp | --agents | --secrets | --all (default)]"
11
+ disable-model-invocation: true
12
+ allowed-tools: Read, Grep, Bash, Glob
13
+ ---
14
+
15
+ # /sentinel — Environment Security Scan
16
+
17
+ $ARGUMENTS
18
+
19
+ ---
20
+
21
+ **EnterPlanMode** — this command is read-only. No file modifications.
22
+
23
+ ---
24
+
25
+ ## Agent Dispatch
26
+
27
+ Check if `security-auditor` agent is installed:
28
+ ```bash
29
+ ls .claude/agents/security-auditor.md 2>/dev/null && echo "agent=found" || echo "agent=missing"
30
+ ```
31
+
32
+ If `agent=found`:
33
+ ```
34
+ Spawn security-auditor agent with:
35
+ Task: Full security scan — all 5 categories, 102 rules
36
+ Scope: $ARGUMENTS (default: --all)
37
+ Return: Security Report in standard format
38
+ ```
39
+ Display the returned Security Report and **ExitPlanMode**. Done — do not run layers below.
40
+
41
+ If `agent=missing`: continue with manual layers below.
42
+
43
+ ---
44
+
45
+ ## Overview (fallback — no agent installed)
46
+
47
+ Scans five layers of the Claude Code environment for security issues.
48
+ Each layer is scored independently. Final score = weighted average (0–100).
49
+ Grade: A ≥ 90 · B ≥ 75 · C ≥ 60 · D ≥ 45 · F < 45
50
+
51
+ Parse $ARGUMENTS:
52
+ - `--hooks` → run Layer 1 + 2 only
53
+ - `--mcp` → run Layer 3 only
54
+ - `--agents` → run Layer 4 only
55
+ - `--secrets` → run Layer 5 only
56
+ - blank / `--all` → run all five layers
57
+
58
+ ---
59
+
60
+ ## Layer 1 — Hook Integrity (weight: 25)
61
+
62
+ Check if hooks were modified outside of AZCLAUDE.
63
+
64
+ ```bash
65
+ INTEGRITY="$HOME/.claude/.azclaude-integrity"
66
+ SETTINGS="$HOME/.claude/settings.json"
67
+ [ -f "$INTEGRITY" ] && echo "integrity_file=found" || echo "integrity_file=missing"
68
+ [ -f "$SETTINGS" ] && echo "settings_file=found" || echo "settings_file=missing"
69
+ ```
70
+
71
+ If both exist:
72
+ ```bash
73
+ cat "$HOME/.claude/.azclaude-integrity"
74
+ ```
75
+ Compute SHA-256 of the `hooks` key in settings.json and compare.
76
+ - Match → +25 pts — "Hook integrity verified"
77
+ - Mismatch → +0 pts — **BLOCK** "Hook integrity mismatch — hooks modified outside AZCLAUDE"
78
+ - Missing integrity file → +15 pts — "No integrity baseline (run `npx azclaude install` to establish one)"
79
+
80
+ Check each hook script for dangerous patterns:
81
+ ```bash
82
+ ls .claude/hooks/ 2>/dev/null || ls "$HOME/.claude/hooks/" 2>/dev/null
83
+ ```
84
+
85
+ For each `.js` / `.sh` hook found, flag:
86
+ - `curl.*\| sh` or `wget.*\| bash` → **HIGH** — data exfiltration or remote code execution
87
+ - `process\.exit\(0\)` as only exit path in a blocking hook → MEDIUM — hook may be neutered
88
+ - `rm -rf` / `del /f` → **HIGH** — destructive operation in hook
89
+ - External URLs (`https://` in a hook that isn't the AZCLAUDE template) → MEDIUM — review intent
90
+
91
+ ---
92
+
93
+ ## Layer 2 — Permission Audit (weight: 20)
94
+
95
+ Check Claude Code settings for over-permissioned configurations.
96
+
97
+ ```bash
98
+ cat "$HOME/.claude/settings.json" 2>/dev/null | head -80
99
+ cat .claude/settings.local.json 2>/dev/null
100
+ ```
101
+
102
+ Flag these patterns:
103
+ | Pattern | Severity | Finding |
104
+ |---|---|---|
105
+ | `"allowedTools": ["*"]` or wildcard | HIGH | Unrestricted tool access |
106
+ | `"dangerouslyAllowedTools"` present | HIGH | Review each entry |
107
+ | `"bypassPermissionsModeAccepted": true` | HIGH | Permission bypass enabled |
108
+ | No `hooks` key present | MEDIUM | No hook protection installed |
109
+ | `_azclaude: true` absent from hooks | LOW | Hook origin unverified |
110
+
111
+ Score: start at 20, subtract per finding: HIGH −8, MEDIUM −3, LOW −1 (floor: 0)
112
+
113
+ ---
114
+
115
+ ## Layer 3 — MCP Server Scan (weight: 20)
116
+
117
+ ```bash
118
+ cat .mcp.json 2>/dev/null
119
+ cat "$HOME/.claude/mcp.json" 2>/dev/null
120
+ ```
121
+
122
+ For each MCP server entry, check:
123
+ - **Hardcoded secrets** — any value matching `AKIA|sk-|ghp_|glpat-|xoxb-|npm_|AIza|sk_live_|SG\.|-----BEGIN` → **HIGH BLOCK**
124
+ - **Missing env var syntax** — secrets should use `${ENV_VAR}` not raw strings
125
+ - **`npx` + unknown package** — flag packages not in npm registry for manual review
126
+ - **`uvx` / `python -m`** — Python MCP servers: flag if no checksum verification
127
+ - **External URLs in `args`** — remote server connections without allowlist
128
+
129
+ Score: start at 20, subtract HIGH −10, MEDIUM −4, LOW −1 (floor: 0)
130
+
131
+ ---
132
+
133
+ ## Layer 4 — Agent Config Review (weight: 15)
134
+
135
+ ```bash
136
+ ls .claude/agents/*.md 2>/dev/null
137
+ ls templates/agents/*.md 2>/dev/null
138
+ ```
139
+
140
+ For each agent file found, check the system prompt / instructions for:
141
+ - **`ignore.*previous.*instructions`** → HIGH — prompt injection planted
142
+ - **`curl.*\|.*bash`** or `wget.*\|.*sh` → HIGH — RCE instruction
143
+ - **`you are now`** / `pretend you are` → MEDIUM — persona hijack
144
+ - **`<script>`** / HTML injection → MEDIUM — XSS via context
145
+ - **Base64 blocks > 200 chars** → MEDIUM — encoded payload
146
+ - Write-permitted reviewer agents → MEDIUM — violates least-privilege
147
+
148
+ ```bash
149
+ grep -rl "ignore.*previous\|you are now\|curl.*|.*bash" .claude/agents/ 2>/dev/null
150
+ grep -rl "ignore.*previous\|you are now\|curl.*|.*bash" templates/agents/ 2>/dev/null
151
+ ```
152
+
153
+ Score: start at 15, subtract HIGH −10, MEDIUM −4, LOW −1 (floor: 0)
154
+
155
+ ---
156
+
157
+ ## Layer 5 — Secrets Scan (weight: 20)
158
+
159
+ Scan committed and staged files for exposed credentials.
160
+
161
+ ```bash
162
+ git diff --cached --name-only 2>/dev/null
163
+ git ls-files --cached 2>/dev/null | grep -v node_modules | grep -v .git | head -200
164
+ ```
165
+
166
+ Run pattern scan across tracked files:
167
+ ```bash
168
+ grep -rn \
169
+ "AKIA[A-Z0-9]\{16\}\|glpat-[A-Za-z0-9_-]\{20\}\|ghp_[A-Za-z0-9]\{36\}" \
170
+ --include='*.js' --include='*.ts' --include='*.py' --include='*.json' \
171
+ --include='*.yaml' --include='*.yml' --include='*.env' --include='*.sh' \
172
+ . 2>/dev/null | grep -v node_modules | grep -v ".git/"
173
+ ```
174
+
175
+ Also scan for:
176
+ - `xoxb-` (Slack bot), `xoxp-` (Slack user), `npm_` (npm token)
177
+ - `AIza[0-9A-Za-z-_]{35}` (Google API key)
178
+ - `sk_live_` (Stripe secret), `SG\.` (SendGrid)
179
+ - `-----BEGIN.*PRIVATE KEY` (private keys)
180
+
181
+ If `.env` exists: check it is in `.gitignore`:
182
+ ```bash
183
+ grep -q "\.env" .gitignore 2>/dev/null && echo ".env gitignored: yes" || echo ".env gitignored: NO"
184
+ ```
185
+
186
+ Score: start at 20, subtract per finding: HIGH −15, MEDIUM −5 (floor: 0)
187
+ Any hardcoded secret → **BLOCK** — do not allow ship/deploy until resolved.
188
+
189
+ ---
190
+
191
+ ## Scoring & Report
192
+
193
+ Calculate total score:
194
+ ```
195
+ total = layer1_score + layer2_score + layer3_score + layer4_score + layer5_score
196
+ grade = A if total >= 90, B if >= 75, C if >= 60, D if >= 45, else F
197
+ ```
198
+
199
+ Output format:
200
+ ```
201
+ ╔══════════════════════════════════════════════════╗
202
+ ║ SENTINEL — Environment Security ║
203
+ ╚══════════════════════════════════════════════════╝
204
+
205
+ Layer 1 — Hook Integrity ··/25 [status]
206
+ Layer 2 — Permission Audit ··/20 [status]
207
+ Layer 3 — MCP Server Scan ··/20 [status]
208
+ Layer 4 — Agent Config Review ··/15 [status]
209
+ Layer 5 — Secrets Scan ··/20 [status]
210
+ ─────────────────────────────────────────────────
211
+ Total Score: ··/100 Grade: [A/B/C/D/F]
212
+
213
+ BLOCKING FINDINGS:
214
+ [file:line — description — MUST FIX BEFORE SHIP]
215
+
216
+ WARNINGS:
217
+ [file:line — description — review recommended]
218
+
219
+ PASSED:
220
+ [N checks passed with no issues]
221
+ ```
222
+
223
+ **Rules:**
224
+ - Any BLOCK finding → output `VERDICT: BLOCKED` — `/ship` must not proceed
225
+ - Grade A or B, no blocks → output `VERDICT: CLEAR`
226
+ - Grade C/D, no blocks → output `VERDICT: PROCEED WITH CAUTION`
227
+
228
+ **ExitPlanMode**
229
+
230
+ Do not suggest fixes inline. List findings only. User resolves — then re-run `/sentinel`.
@@ -37,6 +37,22 @@ If problem-architect not installed OR git diff is only docs/config: skip and pro
37
37
 
38
38
  ## Pre-Ship Gate (runs before any commit)
39
39
 
40
+ **0. Security scan** — check if `security-auditor` agent is installed:
41
+ ```bash
42
+ ls .claude/agents/security-auditor.md 2>/dev/null && echo "agent=found" || echo "agent=missing"
43
+ ```
44
+ If `agent=found`: spawn `security-auditor` agent. If verdict is `BLOCKED` → STOP.
45
+ ```
46
+ ✗ Pre-ship blocked: security-auditor found BLOCKED findings. Run /sentinel for details.
47
+ ```
48
+ If `agent=missing`: run inline secret scan:
49
+ ```bash
50
+ grep -rn "AKIA[A-Z0-9]\{16\}\|ghp_[A-Za-z0-9]\{36\}\|glpat-\|xoxb-\|sk_live_\|-----BEGIN.*PRIVATE KEY" \
51
+ --include='*.js' --include='*.ts' --include='*.py' --include='*.json' \
52
+ . 2>/dev/null | grep -v node_modules | grep -v ".git/"
53
+ ```
54
+ If any match: STOP. `✗ Pre-ship blocked: hardcoded secret detected. Fix before shipping.`
55
+
40
56
  **1. IDE diagnostics** — use `mcp__ide__getDiagnostics` if available.
41
57
  If unavailable or empty: skip this check.
42
58
  If errors exist: STOP.
@@ -96,7 +96,7 @@ const RULES = [
96
96
  },
97
97
  {
98
98
  id: 'hardcoded-secret',
99
- test: /AKIA[A-Z0-9]{16}|sk-[a-z0-9]{20,}|ghp_[A-Za-z0-9]{36}/,
99
+ test: /AKIA[A-Z0-9]{16}|sk-[a-zA-Z0-9]{20,}|ghp_[A-Za-z0-9]{36}|glpat-[A-Za-z0-9_-]{20}|xoxb-[0-9]|xoxp-[0-9]|npm_[A-Za-z0-9]{36}|AIza[0-9A-Za-z_-]{35}|sk_live_[0-9a-zA-Z]{24}|SG\.[A-Za-z0-9_-]{22}\.|-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY/,
100
100
  message: 'Hardcoded secret pattern detected',
101
101
  block: true,
102
102
  },
@@ -94,6 +94,22 @@ if (dTrimIdx !== -1) {
94
94
  content = content.replace(/^Updated: .*/m, `Updated: ${today}`);
95
95
  try { fs.writeFileSync(goalsPath, content); } catch (_) {}
96
96
 
97
+ // ── Prune old checkpoints — keep 5 most recent, delete the rest ──────────────
98
+ // Older checkpoints are superseded by goals.md "Current threads" entries.
99
+ const checkpointDir = path.join(cfg, 'memory', 'checkpoints');
100
+ if (fs.existsSync(checkpointDir)) {
101
+ try {
102
+ const cpFiles = fs.readdirSync(checkpointDir)
103
+ .filter(f => f.endsWith('.md'))
104
+ .sort()
105
+ .reverse(); // newest first (YYYY-MM-DD-HH-MM.md sorts correctly)
106
+ const MAX_CHECKPOINTS = 5;
107
+ for (const f of cpFiles.slice(MAX_CHECKPOINTS)) {
108
+ try { fs.unlinkSync(path.join(checkpointDir, f)); } catch (_) {}
109
+ }
110
+ } catch (_) {}
111
+ }
112
+
97
113
  // ── Reset edit counter so checkpoint reminder starts fresh next session ───────
98
114
  const counterPath = path.join(os.tmpdir(), `.azclaude-edit-count-${process.ppid || process.pid}`);
99
115
  try { fs.writeFileSync(counterPath, '0'); } catch (_) {}