azclaude-copilot 0.4.39 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +9 -7
- package/bin/cli.js +53 -1
- package/package.json +2 -2
- package/templates/CLAUDE.md +35 -1
- package/templates/agents/cc-cli-integrator.md +5 -0
- package/templates/agents/cc-template-author.md +7 -0
- package/templates/agents/cc-test-maintainer.md +5 -0
- package/templates/agents/code-reviewer.md +11 -0
- package/templates/agents/constitution-guard.md +9 -0
- package/templates/agents/devops-engineer.md +9 -0
- package/templates/agents/loop-controller.md +7 -0
- package/templates/agents/milestone-builder.md +7 -0
- package/templates/agents/orchestrator-init.md +9 -1
- package/templates/agents/orchestrator.md +8 -0
- package/templates/agents/problem-architect.md +29 -1
- package/templates/agents/qa-engineer.md +9 -0
- package/templates/agents/security-auditor.md +9 -0
- package/templates/agents/spec-reviewer.md +9 -0
- package/templates/agents/test-writer.md +11 -0
- package/templates/capabilities/manifest.md +2 -0
- package/templates/capabilities/shared/context-inoculation.md +39 -0
- package/templates/capabilities/shared/reward-hack-detection.md +32 -0
- package/templates/commands/audit.md +8 -0
- package/templates/commands/ghost-test.md +99 -0
- package/templates/commands/inoculate.md +76 -0
- package/templates/commands/sentinel.md +3 -0
- package/templates/commands/ship.md +6 -0
- package/templates/commands/test.md +10 -0
- package/templates/hooks/post-tool-use.js +341 -277
- package/templates/hooks/pre-tool-use.js +344 -292
- package/templates/hooks/stop.js +198 -151
- package/templates/hooks/user-prompt.js +369 -163
- package/templates/scripts/statusline.sh +105 -0
- package/templates/skills/agent-creator/SKILL.md +11 -0
- package/templates/skills/architecture-advisor/SKILL.md +21 -16
- package/templates/skills/debate/SKILL.md +5 -0
- package/templates/skills/env-scanner/SKILL.md +5 -0
- package/templates/skills/frontend-design/SKILL.md +5 -0
- package/templates/skills/mcp/SKILL.md +3 -0
- package/templates/skills/security/SKILL.md +3 -0
- package/templates/skills/session-guard/SKILL.md +3 -0
- package/templates/skills/skill-creator/SKILL.md +12 -0
- package/templates/skills/test-first/SKILL.md +5 -0
|
@@ -1,277 +1,341 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
/**
|
|
4
|
-
* AZCLAUDE — PostToolUse hook
|
|
5
|
-
* Tracks edits to goals.md after every Write/Edit.
|
|
6
|
-
* Captures: timestamp, file path, git diff stat (+N/-N), and change summary.
|
|
7
|
-
* Survives Claude Code context compaction — goals.md is the external memory.
|
|
8
|
-
* No user action required. Silent. Works on Windows/macOS/Linux.
|
|
9
|
-
*/
|
|
10
|
-
const fs = require('fs');
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const os = require('os');
|
|
13
|
-
const { spawnSync } = require('child_process');
|
|
14
|
-
|
|
15
|
-
// ── Hook profile gate ───────────────────────────────────────────────────────
|
|
16
|
-
// AZCLAUDE_HOOK_PROFILE=minimal|standard|strict (default: standard)
|
|
17
|
-
// minimal = goals.md tracking only (no observations, no cost tracking)
|
|
18
|
-
// standard = all features (default)
|
|
19
|
-
// strict = all features + extra validation
|
|
20
|
-
const HOOK_PROFILE = process.env.AZCLAUDE_HOOK_PROFILE || 'standard';
|
|
21
|
-
|
|
22
|
-
// Read tool input + response from stdin — Claude Code sends JSON and closes stdin
|
|
23
|
-
let filePath = '';
|
|
24
|
-
let changeSummary = '';
|
|
25
|
-
let toolName = '';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
try { fs.
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
file
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* AZCLAUDE — PostToolUse hook
|
|
5
|
+
* Tracks edits to goals.md after every Write/Edit.
|
|
6
|
+
* Captures: timestamp, file path, git diff stat (+N/-N), and change summary.
|
|
7
|
+
* Survives Claude Code context compaction — goals.md is the external memory.
|
|
8
|
+
* No user action required. Silent. Works on Windows/macOS/Linux.
|
|
9
|
+
*/
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const { spawnSync } = require('child_process');
|
|
14
|
+
|
|
15
|
+
// ── Hook profile gate ───────────────────────────────────────────────────────
|
|
16
|
+
// AZCLAUDE_HOOK_PROFILE=minimal|standard|strict (default: standard)
|
|
17
|
+
// minimal = goals.md tracking only (no observations, no cost tracking)
|
|
18
|
+
// standard = all features (default)
|
|
19
|
+
// strict = all features + extra validation
|
|
20
|
+
const HOOK_PROFILE = process.env.AZCLAUDE_HOOK_PROFILE || 'standard';
|
|
21
|
+
|
|
22
|
+
// Read tool input + response from stdin — Claude Code sends JSON and closes stdin
|
|
23
|
+
let filePath = '';
|
|
24
|
+
let changeSummary = '';
|
|
25
|
+
let toolName = '';
|
|
26
|
+
let toolOutput = '';
|
|
27
|
+
try {
|
|
28
|
+
const raw = fs.readFileSync(0, 'utf8'); // fd 0 = stdin, cross-platform
|
|
29
|
+
const data = JSON.parse(raw);
|
|
30
|
+
toolName = data.tool_name || '';
|
|
31
|
+
filePath = data.tool_input?.file_path || data.tool_input?.path || data.tool_input?.command || '';
|
|
32
|
+
// Extract change summary from old_string/new_string diff hint (Edit tool)
|
|
33
|
+
// MultiEdit: edits[] array — use first edit's new_string
|
|
34
|
+
const oldStr = data.tool_input?.old_string || data.tool_input?.edits?.[0]?.old_string || '';
|
|
35
|
+
const newStr = data.tool_input?.new_string || data.tool_input?.edits?.[0]?.new_string || '';
|
|
36
|
+
// Capture tool result output for Bash secret scanning
|
|
37
|
+
toolOutput = data.tool_result?.output || data.tool_result?.stdout || '';
|
|
38
|
+
if (oldStr && newStr) {
|
|
39
|
+
// Summarize: first non-empty line of new content (what was added)
|
|
40
|
+
const firstNew = newStr.split('\n').find(l => l.trim().length > 0) || '';
|
|
41
|
+
if (firstNew.length > 0 && firstNew.length < 80) {
|
|
42
|
+
changeSummary = ' — ' + firstNew.trim().replace(/^[-*#`]+\s*/, '').slice(0, 60);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (_) {}
|
|
46
|
+
|
|
47
|
+
// Also accept env var fallback (older Claude Code versions)
|
|
48
|
+
if (!filePath) filePath = process.env.CLAUDE_FILE_PATH || '';
|
|
49
|
+
|
|
50
|
+
const cfg = process.env.AZCLAUDE_CFG || '.claude';
|
|
51
|
+
// Guard: cfg must resolve inside the project root
|
|
52
|
+
if (path.resolve(cfg).indexOf(process.cwd()) !== 0) process.exit(0);
|
|
53
|
+
const goalsPath = path.join(cfg, 'memory', 'goals.md');
|
|
54
|
+
if (!fs.existsSync(goalsPath)) process.exit(0); // not an AZCLAUDE project
|
|
55
|
+
|
|
56
|
+
// For non-file tools (Bash, Grep without file_path), still capture observations but skip goals tracking
|
|
57
|
+
const isFileTool = toolName === 'Write' || toolName === 'Edit' || toolName === 'MultiEdit' || (!toolName && filePath);
|
|
58
|
+
const rel = filePath ? path.relative(process.cwd(), path.resolve(filePath)) : toolName || 'unknown';
|
|
59
|
+
|
|
60
|
+
if (isFileTool) {
|
|
61
|
+
if (!filePath) process.exit(0);
|
|
62
|
+
if (rel.startsWith('..')) process.exit(0); // outside project
|
|
63
|
+
if (/goals\.md$/.test(rel)) process.exit(0); // prevent loop
|
|
64
|
+
if (/node_modules[\\/]|\.git[\\/]/.test(rel)) process.exit(0); // noise
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Timestamp HH:MM
|
|
68
|
+
const now = new Date();
|
|
69
|
+
const ts = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
|
70
|
+
|
|
71
|
+
// ── Goals.md tracking (Write/Edit only — file modifications) ────────────────
|
|
72
|
+
if (isFileTool) {
|
|
73
|
+
|
|
74
|
+
// Git diff stat: "+N/-M" — cached for 5s to avoid repeated git calls on consecutive edits
|
|
75
|
+
let diffStat = '';
|
|
76
|
+
const diffCachePath = path.join(os.tmpdir(), `.azclaude-diff-${process.ppid || process.pid}`);
|
|
77
|
+
let diffCache = {};
|
|
78
|
+
try { diffCache = JSON.parse(fs.readFileSync(diffCachePath, 'utf8')); } catch (_) {}
|
|
79
|
+
const cacheAge = Date.now() - (diffCache._ts || 0);
|
|
80
|
+
const cached = diffCache[rel];
|
|
81
|
+
if (cached && cacheAge < 5000) {
|
|
82
|
+
diffStat = cached;
|
|
83
|
+
} else {
|
|
84
|
+
try {
|
|
85
|
+
const r = spawnSync('git', ['diff', 'HEAD', '--numstat', '--', rel],
|
|
86
|
+
{ encoding: 'utf8', cwd: process.cwd(), timeout: 3000 });
|
|
87
|
+
if (r.status === 0 && r.stdout.trim()) {
|
|
88
|
+
const [added, deleted] = r.stdout.trim().split('\t');
|
|
89
|
+
const a = parseInt(added, 10);
|
|
90
|
+
const d = parseInt(deleted, 10);
|
|
91
|
+
if (!isNaN(a) && !isNaN(d) && (a > 0 || d > 0)) {
|
|
92
|
+
diffStat = ` (+${a}/-${d})`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (_) {}
|
|
96
|
+
// Update cache
|
|
97
|
+
if (cacheAge >= 5000) diffCache = {};
|
|
98
|
+
diffCache[rel] = diffStat;
|
|
99
|
+
diffCache._ts = Date.now();
|
|
100
|
+
try { fs.writeFileSync(diffCachePath, JSON.stringify(diffCache)); } catch (_) {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const entry = `- ${ts} — ${rel}${diffStat}${changeSummary}`;
|
|
104
|
+
|
|
105
|
+
let content = fs.readFileSync(goalsPath, 'utf8');
|
|
106
|
+
|
|
107
|
+
const HEADING = '## In progress';
|
|
108
|
+
|
|
109
|
+
if (!content.includes(HEADING)) {
|
|
110
|
+
// Add section at end
|
|
111
|
+
content = content.trimEnd() + `\n\n${HEADING}\n${entry}\n`;
|
|
112
|
+
} else {
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
const hIdx = lines.findIndex(l => l.trim() === HEADING);
|
|
115
|
+
|
|
116
|
+
// Remove any existing entry for the same file (dedup — keep latest timestamp)
|
|
117
|
+
const cleaned = lines.filter((l, i) => {
|
|
118
|
+
if (i <= hIdx) return true; // keep heading and everything before
|
|
119
|
+
if (!l.startsWith('- ')) return true; // keep non-entry lines
|
|
120
|
+
return !l.includes(rel); // remove old entry for this file
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Insert new entry right after heading
|
|
124
|
+
cleaned.splice(hIdx + 1, 0, entry);
|
|
125
|
+
content = cleaned.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try { fs.writeFileSync(goalsPath, content); } catch (_) {}
|
|
129
|
+
|
|
130
|
+
// ── Memory rotation — keep ## In progress bounded at 30 entries ──────────────
|
|
131
|
+
const ROTATE_THRESHOLD = 30;
|
|
132
|
+
const KEEP_NEWEST = 15;
|
|
133
|
+
const rotLines = content.split('\n');
|
|
134
|
+
const rotHIdx = rotLines.findIndex(l => l.trim() === HEADING);
|
|
135
|
+
if (rotHIdx !== -1) {
|
|
136
|
+
const ipEntries = [];
|
|
137
|
+
for (let i = rotHIdx + 1; i < rotLines.length; i++) {
|
|
138
|
+
if (rotLines[i].startsWith('## ')) break;
|
|
139
|
+
if (rotLines[i].startsWith('- ')) ipEntries.push({ line: rotLines[i], idx: i });
|
|
140
|
+
}
|
|
141
|
+
if (ipEntries.length >= ROTATE_THRESHOLD) {
|
|
142
|
+
const toArchive = ipEntries.slice(KEEP_NEWEST);
|
|
143
|
+
const archiveTs = new Date().toISOString().slice(0, 16);
|
|
144
|
+
const archiveDate = new Date().toISOString().slice(0, 10);
|
|
145
|
+
const archivePath = path.join(cfg, 'memory', 'sessions', `${archiveDate}-edits.md`);
|
|
146
|
+
try { fs.mkdirSync(path.join(cfg, 'memory', 'sessions'), { recursive: true }); } catch (_) {}
|
|
147
|
+
const header = `\n<!-- archived: ${archiveTs} source: post-tool-use -->\n`;
|
|
148
|
+
const payload = toArchive.map(e => e.line).join('\n') + '\n';
|
|
149
|
+
try { fs.appendFileSync(archivePath, header + payload); } catch (_) {}
|
|
150
|
+
// Rewrite goals.md keeping only newest 15 entries
|
|
151
|
+
const archivedSet = new Set(toArchive.map(e => e.idx));
|
|
152
|
+
const pruned = rotLines.filter((_, i) => !archivedSet.has(i));
|
|
153
|
+
try { fs.writeFileSync(goalsPath, pruned.join('\n')); } catch (_) {}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
} // end isFileTool goals tracking
|
|
158
|
+
|
|
159
|
+
// ── Reflex observation capture (standard/strict only) ───────────────────────
|
|
160
|
+
// Append tool-use observation to observations.jsonl for pattern detection.
|
|
161
|
+
// Tracks actual tool name + tool sequences (last 3 tools) for pattern detection.
|
|
162
|
+
if (HOOK_PROFILE !== 'minimal') {
|
|
163
|
+
const reflexDir = path.join(cfg, 'memory', 'reflexes');
|
|
164
|
+
const obsTs = now.toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
165
|
+
const tool = toolName || 'Edit';
|
|
166
|
+
const safeRel = rel.replace(/\.(env|key|pem|secret|credential)/gi, '.[REDACTED]');
|
|
167
|
+
|
|
168
|
+
// ── Reflex observation capture ──
|
|
169
|
+
try {
|
|
170
|
+
fs.mkdirSync(reflexDir, { recursive: true });
|
|
171
|
+
const obsPath = path.join(reflexDir, 'observations.jsonl');
|
|
172
|
+
|
|
173
|
+
// Track tool sequence: last 3 tools for pattern detection (Read→Edit→Bash)
|
|
174
|
+
const seqPath = path.join(os.tmpdir(), `.azclaude-seq-${process.ppid || process.pid}`);
|
|
175
|
+
let seq = [];
|
|
176
|
+
try { seq = JSON.parse(fs.readFileSync(seqPath, 'utf8')); } catch (_) {}
|
|
177
|
+
seq.push(tool);
|
|
178
|
+
if (seq.length > 3) seq = seq.slice(-3);
|
|
179
|
+
try { fs.writeFileSync(seqPath, JSON.stringify(seq)); } catch (_) {}
|
|
180
|
+
|
|
181
|
+
const obs = JSON.stringify({
|
|
182
|
+
ts: obsTs, tool, file: safeRel, session: process.ppid || process.pid,
|
|
183
|
+
event: 'complete', seq: seq.join('→')
|
|
184
|
+
});
|
|
185
|
+
fs.appendFileSync(obsPath, obs + '\n');
|
|
186
|
+
|
|
187
|
+
// Auto-truncate: stat-based size check (avoids reading entire file every call)
|
|
188
|
+
try {
|
|
189
|
+
const obsStat = fs.statSync(obsPath);
|
|
190
|
+
if (obsStat.size > 200000) { // ~200KB ≈ ~2000 lines
|
|
191
|
+
const obsContent = fs.readFileSync(obsPath, 'utf8');
|
|
192
|
+
const obsLines = obsContent.split('\n').filter(Boolean);
|
|
193
|
+
fs.writeFileSync(obsPath, obsLines.slice(-500).join('\n') + '\n');
|
|
194
|
+
}
|
|
195
|
+
} catch (_) {}
|
|
196
|
+
} catch (_) {}
|
|
197
|
+
|
|
198
|
+
// ── Behavioral security: sequence detection ──
|
|
199
|
+
try {
|
|
200
|
+
const secSeqPath = path.join(os.tmpdir(), `.azclaude-secseq-${process.ppid || process.pid}`);
|
|
201
|
+
let secSeq = [];
|
|
202
|
+
try { secSeq = JSON.parse(fs.readFileSync(secSeqPath, 'utf8')); } catch (_) {}
|
|
203
|
+
secSeq.push({ tool, file: rel });
|
|
204
|
+
if (secSeq.length > 5) secSeq = secSeq.slice(-5);
|
|
205
|
+
try { fs.writeFileSync(secSeqPath, JSON.stringify(secSeq)); } catch (_) {}
|
|
206
|
+
|
|
207
|
+
if (secSeq.length >= 2) {
|
|
208
|
+
const prev = secSeq[secSeq.length - 2];
|
|
209
|
+
const curr = secSeq[secSeq.length - 1];
|
|
210
|
+
const CRED = /\.env$|secrets?\.(json|ya?ml)$|credentials?(\.json)?$|id_rsa$|\.pem$/i;
|
|
211
|
+
// Pattern: Read credential file → Bash or WebFetch
|
|
212
|
+
if (prev.tool === 'Read' && CRED.test(prev.file || '')
|
|
213
|
+
&& (curr.tool === 'Bash' || curr.tool === 'WebFetch')) {
|
|
214
|
+
const seclogPath = path.join(os.tmpdir(), `.azclaude-seclog-${process.ppid || process.pid}`);
|
|
215
|
+
const entry = JSON.stringify({
|
|
216
|
+
ts: obsTs, hook: 'post-tool-use',
|
|
217
|
+
rule: 'credential-read-then-exec', level: 'warn',
|
|
218
|
+
target: `${path.basename(prev.file || '')} → ${curr.tool}`
|
|
219
|
+
});
|
|
220
|
+
try { fs.appendFileSync(seclogPath, entry + '\n'); } catch (_) {}
|
|
221
|
+
process.stderr.write(
|
|
222
|
+
`\n⚠ SECURITY: Credential file (${path.basename(prev.file || '')}) read then ${curr.tool} — verify no secrets are being transmitted.\n`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Reward hack behavioral patterns (Anthropic "Emergent Misalignment" paper) ──
|
|
228
|
+
|
|
229
|
+
// Pattern: Bash(test run) → Edit/Write(test file) = possible reward hacking
|
|
230
|
+
if (secSeq.length >= 2) {
|
|
231
|
+
const prev2 = secSeq[secSeq.length - 2];
|
|
232
|
+
const curr2 = secSeq[secSeq.length - 1];
|
|
233
|
+
if (prev2.tool === 'Bash' && /\b(pytest|jest|mocha|vitest|npm\s+test|npx\s+test)\b/i.test(prev2.file || '')
|
|
234
|
+
&& (curr2.tool === 'Edit' || curr2.tool === 'Write' || curr2.tool === 'MultiEdit')
|
|
235
|
+
&& /test[_/\\]|_test\.|\.test\.|\.spec\.|conftest/i.test(curr2.file || '')) {
|
|
236
|
+
const seclogPath = path.join(os.tmpdir(), `.azclaude-seclog-${process.ppid || process.pid}`);
|
|
237
|
+
const entry2 = JSON.stringify({
|
|
238
|
+
ts: obsTs, hook: 'post-tool-use',
|
|
239
|
+
rule: 'test-then-test-modify', level: 'warn',
|
|
240
|
+
target: `${prev2.tool}(test) → ${curr2.tool}(${path.basename(curr2.file || '')})`
|
|
241
|
+
});
|
|
242
|
+
try { fs.appendFileSync(seclogPath, entry2 + '\n'); } catch (_) {}
|
|
243
|
+
process.stderr.write(
|
|
244
|
+
`\n⚠ SECURITY: Test run then test file modification — verify edits fix the code, not fake the result.\n`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Pattern: Any Edit/Write to .claude/hooks/ = always warn (hook self-modification)
|
|
250
|
+
{
|
|
251
|
+
const currH = secSeq[secSeq.length - 1];
|
|
252
|
+
if (currH && (currH.tool === 'Edit' || currH.tool === 'Write' || currH.tool === 'MultiEdit')
|
|
253
|
+
&& /\.claude[/\\]hooks[/\\]/i.test(currH.file || '')) {
|
|
254
|
+
const seclogPath = path.join(os.tmpdir(), `.azclaude-seclog-${process.ppid || process.pid}`);
|
|
255
|
+
const entryH = JSON.stringify({
|
|
256
|
+
ts: obsTs, hook: 'post-tool-use',
|
|
257
|
+
rule: 'hook-self-modification', level: 'warn',
|
|
258
|
+
target: path.basename(currH.file || '')
|
|
259
|
+
});
|
|
260
|
+
try { fs.appendFileSync(seclogPath, entryH + '\n'); } catch (_) {}
|
|
261
|
+
process.stderr.write(
|
|
262
|
+
`\n⚠ SECURITY: Hook file modified (${path.basename(currH.file || '')}) — hooks control all tool execution. Verify this change is intentional.\n`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} catch (_) {}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── Cost tracking (standard/strict only) ────────────────────────────────────
|
|
270
|
+
// Append estimated cost per tool call to costs.jsonl for budget awareness.
|
|
271
|
+
if (HOOK_PROFILE !== 'minimal') {
|
|
272
|
+
try {
|
|
273
|
+
const costsDir = path.join(cfg, 'memory', 'metrics');
|
|
274
|
+
fs.mkdirSync(costsDir, { recursive: true });
|
|
275
|
+
const costsPath = path.join(costsDir, 'costs.jsonl');
|
|
276
|
+
const costEntry = JSON.stringify({
|
|
277
|
+
ts: now.toISOString().replace(/\.\d{3}Z$/, 'Z'),
|
|
278
|
+
tool: toolName || 'Edit',
|
|
279
|
+
file: rel,
|
|
280
|
+
session: process.ppid || process.pid
|
|
281
|
+
});
|
|
282
|
+
fs.appendFileSync(costsPath, costEntry + '\n');
|
|
283
|
+
// Auto-truncate: stat-based size check
|
|
284
|
+
try {
|
|
285
|
+
const costStat = fs.statSync(costsPath);
|
|
286
|
+
if (costStat.size > 100000) { // ~100KB ≈ ~1000 entries
|
|
287
|
+
const costLines = fs.readFileSync(costsPath, 'utf8').split('\n').filter(Boolean);
|
|
288
|
+
fs.writeFileSync(costsPath, costLines.slice(-500).join('\n') + '\n');
|
|
289
|
+
}
|
|
290
|
+
} catch (_) {}
|
|
291
|
+
} catch (_) {}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Bash output secret scanning (standard/strict only) ──────────────────────
|
|
295
|
+
if (HOOK_PROFILE !== 'minimal' && toolName === 'Bash' && toolOutput) {
|
|
296
|
+
const SECRET_RE = /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]|-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY/;
|
|
297
|
+
if (SECRET_RE.test(toolOutput)) {
|
|
298
|
+
const seclogPath = path.join(os.tmpdir(), `.azclaude-seclog-${process.ppid || process.pid}`);
|
|
299
|
+
const entry = JSON.stringify({
|
|
300
|
+
ts: now.toISOString(), hook: 'post-tool-use',
|
|
301
|
+
rule: 'bash-output-secret-leak', level: 'warn',
|
|
302
|
+
target: (filePath || '').slice(0, 80)
|
|
303
|
+
});
|
|
304
|
+
try { fs.appendFileSync(seclogPath, entry + '\n'); } catch (_) {}
|
|
305
|
+
process.stderr.write(
|
|
306
|
+
`\n⚠ SECURITY: Bash output contains a secret pattern — verify no credentials were leaked to logs or context.\n`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ── Checkpoint reminder every 15 edits ──────────────────────────────────────
|
|
312
|
+
const counterPath = path.join(os.tmpdir(), `.azclaude-edit-count-${process.ppid || process.pid}`);
|
|
313
|
+
let editCount = 1;
|
|
314
|
+
try { editCount = parseInt(fs.readFileSync(counterPath, 'utf8'), 10) + 1; } catch (_) {}
|
|
315
|
+
try { fs.writeFileSync(counterPath, String(editCount)); } catch (_) {}
|
|
316
|
+
if (editCount > 0 && editCount % 15 === 0) {
|
|
317
|
+
process.stderr.write(`\n⚠ ${editCount} edits this session — run /snapshot before context compaction loses your reasoning\n`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Rapid-edit detection — same file edited 5+ times in <5 min ───────────────
|
|
321
|
+
// Signal: unclear spec before coding. Warn once, suggest /blueprint.
|
|
322
|
+
if (isFileTool && rel) {
|
|
323
|
+
const rapidPath = path.join(os.tmpdir(), `.azclaude-rapid-${process.ppid || process.pid}`);
|
|
324
|
+
let rapidLog = {};
|
|
325
|
+
try { rapidLog = JSON.parse(fs.readFileSync(rapidPath, 'utf8')); } catch (_) {}
|
|
326
|
+
const fileLog = rapidLog[rel] || { count: 0, firstTs: Date.now(), warned: false };
|
|
327
|
+
const elapsed = Date.now() - fileLog.firstTs;
|
|
328
|
+
if (elapsed > 5 * 60 * 1000) {
|
|
329
|
+
// Reset window
|
|
330
|
+
rapidLog[rel] = { count: 1, firstTs: Date.now(), warned: false };
|
|
331
|
+
} else {
|
|
332
|
+
fileLog.count += 1;
|
|
333
|
+
if (fileLog.count >= 5 && !fileLog.warned) {
|
|
334
|
+
fileLog.warned = true;
|
|
335
|
+
const shortName = path.basename(rel);
|
|
336
|
+
process.stdout.write(`\n⚠ ${fileLog.count} edits to ${shortName} in ${Math.round(elapsed/60000)}min — unclear spec? Consider /blueprint before continuing\n`);
|
|
337
|
+
}
|
|
338
|
+
rapidLog[rel] = fileLog;
|
|
339
|
+
}
|
|
340
|
+
try { fs.writeFileSync(rapidPath, JSON.stringify(rapidLog)); } catch (_) {}
|
|
341
|
+
}
|