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 +2 -2
- package/bin/cli.js +2 -2
- package/bin/copilot.js +96 -9
- package/package.json +1 -1
- package/templates/agents/security-auditor.md +395 -0
- package/templates/commands/sentinel.md +230 -0
- package/templates/commands/ship.md +16 -0
- package/templates/hooks/pre-tool-use.js +1 -1
- package/templates/hooks/stop.js +16 -0
package/README.md
CHANGED
|
@@ -476,11 +476,11 @@ See [SECURITY.md](SECURITY.md) for full details.
|
|
|
476
476
|
|
|
477
477
|
## Verified
|
|
478
478
|
|
|
479
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
@@ -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-
|
|
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
|
},
|
package/templates/hooks/stop.js
CHANGED
|
@@ -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 (_) {}
|