@paths.design/caws-cli 8.0.1 → 8.1.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/dist/commands/archive.d.ts +2 -1
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/archive.js +114 -6
- package/dist/commands/burnup.d.ts.map +1 -1
- package/dist/commands/burnup.js +109 -10
- package/dist/commands/diagnose.js +1 -1
- package/dist/commands/mode.js +24 -14
- package/dist/commands/provenance.js +216 -93
- package/dist/commands/quality-gates.d.ts.map +1 -1
- package/dist/commands/quality-gates.js +3 -1
- package/dist/commands/specs.js +184 -6
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +134 -10
- package/dist/commands/templates.js +2 -2
- package/dist/error-handler.js +6 -98
- package/dist/generators/jest-config-generator.js +242 -0
- package/dist/index.js +4 -7
- package/dist/minimal-cli.js +3 -1
- package/dist/scaffold/claude-hooks.js +316 -0
- package/dist/scaffold/index.js +18 -0
- package/dist/templates/.claude/README.md +190 -0
- package/dist/templates/.claude/hooks/audit.sh +96 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +90 -0
- package/dist/templates/.claude/hooks/naming-check.sh +97 -0
- package/dist/templates/.claude/hooks/quality-check.sh +68 -0
- package/dist/templates/.claude/hooks/scan-secrets.sh +85 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +105 -0
- package/dist/templates/.claude/hooks/validate-spec.sh +76 -0
- package/dist/templates/.claude/settings.json +95 -0
- package/dist/test-analysis.js +203 -10
- package/dist/utils/error-categories.js +210 -0
- package/dist/utils/quality-gates-utils.js +402 -0
- package/dist/utils/typescript-detector.js +36 -90
- package/dist/validation/spec-validation.js +59 -6
- package/package.json +5 -3
- package/templates/.claude/README.md +190 -0
- package/templates/.claude/hooks/audit.sh +96 -0
- package/templates/.claude/hooks/block-dangerous.sh +90 -0
- package/templates/.claude/hooks/naming-check.sh +97 -0
- package/templates/.claude/hooks/quality-check.sh +68 -0
- package/templates/.claude/hooks/scan-secrets.sh +85 -0
- package/templates/.claude/hooks/scope-guard.sh +105 -0
- package/templates/.claude/hooks/validate-spec.sh +76 -0
- package/templates/.claude/settings.json +95 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Claude Code Hooks Scaffolding
|
|
3
|
+
* Functions for setting up Claude Code hooks for CAWS projects
|
|
4
|
+
* @author @darianrosebrook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
// Import detection utilities
|
|
12
|
+
const { detectCAWSSetup, findPackageRoot } = require('../utils/detection');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scaffold Claude Code hooks for a CAWS project
|
|
16
|
+
* Creates .claude/settings.json with hooks and .claude/hooks/ directory with scripts
|
|
17
|
+
*
|
|
18
|
+
* @param {string} projectDir - Project directory path
|
|
19
|
+
* @param {string[]} levels - Hook levels to enable: 'safety', 'quality', 'scope', 'audit'
|
|
20
|
+
*/
|
|
21
|
+
async function scaffoldClaudeHooks(projectDir, levels = ['safety', 'quality', 'scope', 'audit']) {
|
|
22
|
+
try {
|
|
23
|
+
const claudeDir = path.join(projectDir, '.claude');
|
|
24
|
+
const claudeHooksDir = path.join(claudeDir, 'hooks');
|
|
25
|
+
|
|
26
|
+
// Create .claude directory structure
|
|
27
|
+
await fs.ensureDir(claudeDir);
|
|
28
|
+
await fs.ensureDir(claudeHooksDir);
|
|
29
|
+
|
|
30
|
+
// Determine template directory - prefer bundled templates
|
|
31
|
+
const setup = detectCAWSSetup(projectDir);
|
|
32
|
+
|
|
33
|
+
// Find package root using shared utility
|
|
34
|
+
const packageRoot = findPackageRoot(__dirname);
|
|
35
|
+
|
|
36
|
+
// Try templates relative to package root first (works in both dev and global install)
|
|
37
|
+
const bundledTemplateDir = path.join(packageRoot, 'templates');
|
|
38
|
+
const fallbackTemplateDir = path.join(__dirname, '../../templates');
|
|
39
|
+
const templateDir = fs.existsSync(bundledTemplateDir)
|
|
40
|
+
? bundledTemplateDir
|
|
41
|
+
: fs.existsSync(fallbackTemplateDir)
|
|
42
|
+
? fallbackTemplateDir
|
|
43
|
+
: setup.templateDir || path.resolve(__dirname, '../templates');
|
|
44
|
+
|
|
45
|
+
const claudeTemplateDir = path.join(templateDir, '.claude');
|
|
46
|
+
const claudeHooksTemplateDir = path.join(claudeTemplateDir, 'hooks');
|
|
47
|
+
|
|
48
|
+
if (!fs.existsSync(claudeTemplateDir)) {
|
|
49
|
+
console.warn(chalk.yellow('⚠️ Claude Code hooks templates not found'));
|
|
50
|
+
console.warn(chalk.blue('💡 Skipping Claude Code hooks setup'));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Map levels to hook scripts
|
|
55
|
+
const hookMapping = {
|
|
56
|
+
safety: ['block-dangerous.sh', 'scan-secrets.sh'],
|
|
57
|
+
quality: ['quality-check.sh', 'validate-spec.sh'],
|
|
58
|
+
scope: ['scope-guard.sh', 'naming-check.sh'],
|
|
59
|
+
audit: ['audit.sh'],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Determine which hooks to enable
|
|
63
|
+
const enabledHooks = new Set();
|
|
64
|
+
levels.forEach((level) => {
|
|
65
|
+
const hooks = hookMapping[level] || [];
|
|
66
|
+
hooks.forEach((hook) => enabledHooks.add(hook));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Always enable audit.sh if any hooks are enabled
|
|
70
|
+
if (enabledHooks.size > 0) {
|
|
71
|
+
enabledHooks.add('audit.sh');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Copy enabled hook scripts
|
|
75
|
+
const allHookScripts = [
|
|
76
|
+
'audit.sh',
|
|
77
|
+
'validate-spec.sh',
|
|
78
|
+
'quality-check.sh',
|
|
79
|
+
'scan-secrets.sh',
|
|
80
|
+
'block-dangerous.sh',
|
|
81
|
+
'scope-guard.sh',
|
|
82
|
+
'naming-check.sh',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
for (const script of allHookScripts) {
|
|
86
|
+
if (enabledHooks.has(script)) {
|
|
87
|
+
const sourcePath = path.join(claudeHooksTemplateDir, script);
|
|
88
|
+
const destPath = path.join(claudeHooksDir, script);
|
|
89
|
+
|
|
90
|
+
if (fs.existsSync(sourcePath)) {
|
|
91
|
+
await fs.copy(sourcePath, destPath);
|
|
92
|
+
// Make executable
|
|
93
|
+
await fs.chmod(destPath, 0o755);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Generate settings.json with hooks configuration
|
|
99
|
+
const settings = generateClaudeSettings(levels, enabledHooks);
|
|
100
|
+
|
|
101
|
+
// Check for existing settings and merge
|
|
102
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
103
|
+
if (fs.existsSync(settingsPath)) {
|
|
104
|
+
try {
|
|
105
|
+
const existingSettings = await fs.readJSON(settingsPath);
|
|
106
|
+
// Merge hooks, preserving existing non-hook settings
|
|
107
|
+
settings.hooks = {
|
|
108
|
+
...existingSettings.hooks,
|
|
109
|
+
...settings.hooks,
|
|
110
|
+
};
|
|
111
|
+
// Preserve other settings
|
|
112
|
+
Object.keys(existingSettings).forEach((key) => {
|
|
113
|
+
if (key !== 'hooks' && !(key in settings)) {
|
|
114
|
+
settings[key] = existingSettings[key];
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.warn(chalk.yellow('⚠️ Could not merge existing settings:'), error.message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Write settings.json
|
|
123
|
+
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
124
|
+
|
|
125
|
+
// Copy README if it exists
|
|
126
|
+
const readmePath = path.join(claudeTemplateDir, 'README.md');
|
|
127
|
+
if (fs.existsSync(readmePath)) {
|
|
128
|
+
await fs.copy(readmePath, path.join(claudeDir, 'README.md'));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(chalk.green('✅ Claude Code hooks configured'));
|
|
132
|
+
console.log(chalk.gray(` Enabled: ${levels.join(', ')}`));
|
|
133
|
+
console.log(
|
|
134
|
+
chalk.gray(` Scripts: ${Array.from(enabledHooks).length} hook scripts installed`)
|
|
135
|
+
);
|
|
136
|
+
console.log(chalk.blue('💡 Hooks will activate on next Claude Code session'));
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(chalk.yellow('⚠️ Failed to setup Claude Code hooks:'), error.message);
|
|
139
|
+
console.log(chalk.blue('💡 You can manually copy .claude/ directory later'));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Generate Claude Code settings with hooks configuration
|
|
145
|
+
* @param {string[]} levels - Enabled hook levels
|
|
146
|
+
* @param {Set<string>} enabledHooks - Set of enabled hook script names
|
|
147
|
+
* @returns {Object} Settings object for settings.json
|
|
148
|
+
*/
|
|
149
|
+
function generateClaudeSettings(levels, _enabledHooks) {
|
|
150
|
+
const settings = {
|
|
151
|
+
hooks: {},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Build hooks configuration based on enabled levels
|
|
155
|
+
// Claude Code uses different event names and matcher patterns
|
|
156
|
+
|
|
157
|
+
if (levels.includes('safety')) {
|
|
158
|
+
// Block dangerous bash commands
|
|
159
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
|
|
160
|
+
settings.hooks.PreToolUse.push({
|
|
161
|
+
matcher: 'Bash',
|
|
162
|
+
hooks: [
|
|
163
|
+
{
|
|
164
|
+
type: 'command',
|
|
165
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/block-dangerous.sh',
|
|
166
|
+
timeout: 10,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Scan for secrets on file read
|
|
172
|
+
settings.hooks.PreToolUse.push({
|
|
173
|
+
matcher: 'Read',
|
|
174
|
+
hooks: [
|
|
175
|
+
{
|
|
176
|
+
type: 'command',
|
|
177
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/scan-secrets.sh',
|
|
178
|
+
timeout: 10,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (levels.includes('quality')) {
|
|
185
|
+
// Run quality checks after file edits
|
|
186
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse || [];
|
|
187
|
+
settings.hooks.PostToolUse.push({
|
|
188
|
+
matcher: 'Write|Edit',
|
|
189
|
+
hooks: [
|
|
190
|
+
{
|
|
191
|
+
type: 'command',
|
|
192
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/quality-check.sh',
|
|
193
|
+
timeout: 30,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
type: 'command',
|
|
197
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/validate-spec.sh',
|
|
198
|
+
timeout: 15,
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (levels.includes('scope')) {
|
|
205
|
+
// Scope guard before file writes
|
|
206
|
+
settings.hooks.PreToolUse = settings.hooks.PreToolUse || [];
|
|
207
|
+
settings.hooks.PreToolUse.push({
|
|
208
|
+
matcher: 'Write|Edit',
|
|
209
|
+
hooks: [
|
|
210
|
+
{
|
|
211
|
+
type: 'command',
|
|
212
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/scope-guard.sh',
|
|
213
|
+
timeout: 10,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Naming check after edits
|
|
219
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse || [];
|
|
220
|
+
settings.hooks.PostToolUse.push({
|
|
221
|
+
matcher: 'Write|Edit',
|
|
222
|
+
hooks: [
|
|
223
|
+
{
|
|
224
|
+
type: 'command',
|
|
225
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/naming-check.sh',
|
|
226
|
+
timeout: 10,
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (levels.includes('audit')) {
|
|
233
|
+
// Session audit logging
|
|
234
|
+
settings.hooks.SessionStart = settings.hooks.SessionStart || [];
|
|
235
|
+
settings.hooks.SessionStart.push({
|
|
236
|
+
hooks: [
|
|
237
|
+
{
|
|
238
|
+
type: 'command',
|
|
239
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/audit.sh session-start',
|
|
240
|
+
timeout: 5,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
settings.hooks.Stop = settings.hooks.Stop || [];
|
|
246
|
+
settings.hooks.Stop.push({
|
|
247
|
+
hooks: [
|
|
248
|
+
{
|
|
249
|
+
type: 'command',
|
|
250
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/audit.sh stop',
|
|
251
|
+
timeout: 5,
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Audit tool usage
|
|
257
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse || [];
|
|
258
|
+
settings.hooks.PostToolUse.push({
|
|
259
|
+
matcher: 'Write|Edit|Bash',
|
|
260
|
+
hooks: [
|
|
261
|
+
{
|
|
262
|
+
type: 'command',
|
|
263
|
+
command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/audit.sh tool-use',
|
|
264
|
+
timeout: 5,
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return settings;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Check if Claude Code hooks are already configured
|
|
275
|
+
* @param {string} projectDir - Project directory
|
|
276
|
+
* @returns {boolean} True if hooks are configured
|
|
277
|
+
*/
|
|
278
|
+
function hasClaudeHooks(projectDir) {
|
|
279
|
+
const settingsPath = path.join(projectDir, '.claude', 'settings.json');
|
|
280
|
+
if (!fs.existsSync(settingsPath)) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const settings = fs.readJSONSync(settingsPath);
|
|
286
|
+
return settings.hooks && Object.keys(settings.hooks).length > 0;
|
|
287
|
+
} catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* List configured Claude Code hooks
|
|
294
|
+
* @param {string} projectDir - Project directory
|
|
295
|
+
* @returns {Object} Hook configuration or null
|
|
296
|
+
*/
|
|
297
|
+
function getClaudeHooksConfig(projectDir) {
|
|
298
|
+
const settingsPath = path.join(projectDir, '.claude', 'settings.json');
|
|
299
|
+
if (!fs.existsSync(settingsPath)) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const settings = fs.readJSONSync(settingsPath);
|
|
305
|
+
return settings.hooks || null;
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
scaffoldClaudeHooks,
|
|
313
|
+
generateClaudeSettings,
|
|
314
|
+
hasClaudeHooks,
|
|
315
|
+
getClaudeHooksConfig,
|
|
316
|
+
};
|
package/dist/scaffold/index.js
CHANGED
|
@@ -16,6 +16,9 @@ const { detectsPublishing } = require('../utils/project-analysis');
|
|
|
16
16
|
const { scaffoldGitHooks } = require('./git-hooks');
|
|
17
17
|
const { updateGitignore } = require('../utils/gitignore-updater');
|
|
18
18
|
|
|
19
|
+
// Import Claude Code hooks scaffolding
|
|
20
|
+
const { scaffoldClaudeHooks } = require('./claude-hooks');
|
|
21
|
+
|
|
19
22
|
// CLI version from package.json
|
|
20
23
|
const CLI_VERSION = require('../../package.json').version;
|
|
21
24
|
|
|
@@ -120,8 +123,22 @@ async function scaffoldIDEIntegrations(targetDir, options) {
|
|
|
120
123
|
dest: '.cursor/README.md',
|
|
121
124
|
desc: 'Cursor integration documentation',
|
|
122
125
|
},
|
|
126
|
+
|
|
127
|
+
// Claude Code hooks
|
|
128
|
+
{
|
|
129
|
+
src: '.claude/README.md',
|
|
130
|
+
dest: '.claude/README.md',
|
|
131
|
+
desc: 'Claude Code integration documentation',
|
|
132
|
+
},
|
|
123
133
|
];
|
|
124
134
|
|
|
135
|
+
// Setup Claude Code hooks
|
|
136
|
+
try {
|
|
137
|
+
await scaffoldClaudeHooks(targetDir, ['safety', 'quality', 'scope', 'audit']);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.log(chalk.yellow(`⚠️ Claude Code hooks setup failed: ${error.message}`));
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
for (const template of ideTemplates) {
|
|
126
143
|
const srcPath = path.join(templateDir, template.src);
|
|
127
144
|
const destPath = path.join(targetDir, template.dest);
|
|
@@ -769,5 +786,6 @@ async function scaffoldProject(options) {
|
|
|
769
786
|
module.exports = {
|
|
770
787
|
scaffoldProject,
|
|
771
788
|
scaffoldIDEIntegrations,
|
|
789
|
+
scaffoldClaudeHooks,
|
|
772
790
|
setScaffoldDependencies,
|
|
773
791
|
};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Claude Code Integration for CAWS
|
|
2
|
+
|
|
3
|
+
This directory contains Claude Code hooks and configuration for CAWS (Coding Agent Working Standard) integration.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
CAWS hooks for Claude Code provide:
|
|
8
|
+
|
|
9
|
+
- **Safety Gates**: Block dangerous commands and scan for secrets
|
|
10
|
+
- **Quality Gates**: Run CAWS quality checks after file edits
|
|
11
|
+
- **Scope Guards**: Validate edits against the working spec's scope
|
|
12
|
+
- **Audit Logging**: Track agent actions for compliance
|
|
13
|
+
|
|
14
|
+
## Directory Structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
.claude/
|
|
18
|
+
├── settings.json # Claude Code settings with hooks configuration
|
|
19
|
+
├── hooks/ # Hook scripts
|
|
20
|
+
│ ├── audit.sh # Session and action logging
|
|
21
|
+
│ ├── block-dangerous.sh # Block destructive commands
|
|
22
|
+
│ ├── scan-secrets.sh # Warn when reading sensitive files
|
|
23
|
+
│ ├── quality-check.sh # Run CAWS quality gates
|
|
24
|
+
│ ├── validate-spec.sh # Validate spec files
|
|
25
|
+
│ ├── scope-guard.sh # Check scope boundaries
|
|
26
|
+
│ └── naming-check.sh # Validate file naming conventions
|
|
27
|
+
├── logs/ # Audit logs (gitignored)
|
|
28
|
+
└── README.md # This file
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Hook Events
|
|
32
|
+
|
|
33
|
+
### PreToolUse Hooks
|
|
34
|
+
|
|
35
|
+
Run before Claude executes a tool:
|
|
36
|
+
|
|
37
|
+
| Hook | Matcher | Purpose |
|
|
38
|
+
|------|---------|---------|
|
|
39
|
+
| `block-dangerous.sh` | `Bash` | Block destructive shell commands |
|
|
40
|
+
| `scan-secrets.sh` | `Read` | Warn when reading sensitive files |
|
|
41
|
+
| `scope-guard.sh` | `Write\|Edit` | Check scope boundaries before edits |
|
|
42
|
+
|
|
43
|
+
### PostToolUse Hooks
|
|
44
|
+
|
|
45
|
+
Run after Claude executes a tool:
|
|
46
|
+
|
|
47
|
+
| Hook | Matcher | Purpose |
|
|
48
|
+
|------|---------|---------|
|
|
49
|
+
| `quality-check.sh` | `Write\|Edit` | Run CAWS quality gates |
|
|
50
|
+
| `validate-spec.sh` | `Write\|Edit` | Validate spec file changes |
|
|
51
|
+
| `naming-check.sh` | `Write` | Check file naming conventions |
|
|
52
|
+
| `audit.sh` | `Write\|Edit\|Bash` | Log tool usage |
|
|
53
|
+
|
|
54
|
+
### Session Hooks
|
|
55
|
+
|
|
56
|
+
| Hook | Event | Purpose |
|
|
57
|
+
|------|-------|---------|
|
|
58
|
+
| `audit.sh session-start` | `SessionStart` | Log session start |
|
|
59
|
+
| `audit.sh stop` | `Stop` | Log session end |
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
### Enable/Disable Hooks
|
|
64
|
+
|
|
65
|
+
Edit `settings.json` to enable or disable specific hooks. Remove entries from the `hooks` object to disable them.
|
|
66
|
+
|
|
67
|
+
### Hook Levels
|
|
68
|
+
|
|
69
|
+
The scaffold supports four hook levels:
|
|
70
|
+
|
|
71
|
+
- **safety**: Block dangerous commands, scan for secrets
|
|
72
|
+
- **quality**: Run quality gates on file edits
|
|
73
|
+
- **scope**: Validate edits against spec scope
|
|
74
|
+
- **audit**: Log all agent actions
|
|
75
|
+
|
|
76
|
+
Run `caws init --hooks=safety,quality` to enable specific levels.
|
|
77
|
+
|
|
78
|
+
## Audit Logs
|
|
79
|
+
|
|
80
|
+
Audit logs are written to `.claude/logs/`:
|
|
81
|
+
|
|
82
|
+
- `audit.log` - All-time log (appended)
|
|
83
|
+
- `audit-YYYY-MM-DD.log` - Daily logs
|
|
84
|
+
|
|
85
|
+
Logs are JSON-formatted for easy parsing:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"timestamp": "2024-01-15T10:30:00Z",
|
|
90
|
+
"session_id": "abc123",
|
|
91
|
+
"event": "tool_use",
|
|
92
|
+
"tool": "Write",
|
|
93
|
+
"file": "src/index.ts",
|
|
94
|
+
"cwd": "/project"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Customization
|
|
99
|
+
|
|
100
|
+
### Adding Custom Hooks
|
|
101
|
+
|
|
102
|
+
1. Create a new script in `.claude/hooks/`
|
|
103
|
+
2. Make it executable: `chmod +x .claude/hooks/my-hook.sh`
|
|
104
|
+
3. Add it to `settings.json`:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"hooks": {
|
|
109
|
+
"PostToolUse": [
|
|
110
|
+
{
|
|
111
|
+
"matcher": "Write|Edit",
|
|
112
|
+
"hooks": [
|
|
113
|
+
{
|
|
114
|
+
"type": "command",
|
|
115
|
+
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/my-hook.sh",
|
|
116
|
+
"timeout": 10
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Hook Input/Output
|
|
126
|
+
|
|
127
|
+
Hooks receive JSON input via stdin:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"session_id": "abc123",
|
|
132
|
+
"hook_event_name": "PostToolUse",
|
|
133
|
+
"tool_name": "Write",
|
|
134
|
+
"tool_input": {
|
|
135
|
+
"file_path": "/path/to/file.ts",
|
|
136
|
+
"content": "..."
|
|
137
|
+
},
|
|
138
|
+
"tool_response": { "success": true }
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Hooks can output JSON to control Claude's behavior:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"decision": "block",
|
|
147
|
+
"reason": "Quality gate failed: ..."
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Or add context:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{
|
|
155
|
+
"hookSpecificOutput": {
|
|
156
|
+
"hookEventName": "PostToolUse",
|
|
157
|
+
"additionalContext": "Remember to update the tests."
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Troubleshooting
|
|
163
|
+
|
|
164
|
+
### Hooks Not Running
|
|
165
|
+
|
|
166
|
+
1. Check `settings.json` syntax: `cat .claude/settings.json | jq .`
|
|
167
|
+
2. Verify scripts are executable: `ls -la .claude/hooks/`
|
|
168
|
+
3. Test hooks manually: `echo '{}' | .claude/hooks/audit.sh`
|
|
169
|
+
|
|
170
|
+
### Permission Errors
|
|
171
|
+
|
|
172
|
+
Make all hook scripts executable:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
chmod +x .claude/hooks/*.sh
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Debug Hooks
|
|
179
|
+
|
|
180
|
+
Run Claude Code with `--debug` to see hook execution details:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
claude --debug
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Further Reading
|
|
187
|
+
|
|
188
|
+
- [Claude Code Hooks Documentation](https://code.claude.com/docs/en/hooks)
|
|
189
|
+
- [CAWS Quality Gates](../../docs/quality-gates.md)
|
|
190
|
+
- [CAWS Scope Management](../../docs/scope-management.md)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# CAWS Audit Hook for Claude Code
|
|
3
|
+
# Logs agent actions for compliance and debugging
|
|
4
|
+
# @author @darianrosebrook
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Get event type from argument or input
|
|
9
|
+
EVENT_TYPE="${1:-tool-use}"
|
|
10
|
+
|
|
11
|
+
# Read JSON input from stdin
|
|
12
|
+
INPUT=$(cat)
|
|
13
|
+
|
|
14
|
+
# Parse common fields from Claude Code hook input
|
|
15
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
16
|
+
CWD=$(echo "$INPUT" | jq -r '.cwd // "."')
|
|
17
|
+
HOOK_EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "unknown"')
|
|
18
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
|
|
19
|
+
PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // "default"')
|
|
20
|
+
|
|
21
|
+
# Ensure log directory exists
|
|
22
|
+
LOG_DIR="${CLAUDE_PROJECT_DIR:-.}/.claude/logs"
|
|
23
|
+
mkdir -p "$LOG_DIR"
|
|
24
|
+
|
|
25
|
+
# Log file path
|
|
26
|
+
LOG_FILE="$LOG_DIR/audit.log"
|
|
27
|
+
DATE_LOG_FILE="$LOG_DIR/audit-$(date +%Y-%m-%d).log"
|
|
28
|
+
|
|
29
|
+
# Timestamp
|
|
30
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
31
|
+
|
|
32
|
+
# Build log entry based on event type
|
|
33
|
+
case "$EVENT_TYPE" in
|
|
34
|
+
session-start)
|
|
35
|
+
SOURCE=$(echo "$INPUT" | jq -r '.source // "unknown"')
|
|
36
|
+
MODEL=$(echo "$INPUT" | jq -r '.model // "unknown"')
|
|
37
|
+
LOG_ENTRY=$(jq -n \
|
|
38
|
+
--arg ts "$TIMESTAMP" \
|
|
39
|
+
--arg sid "$SESSION_ID" \
|
|
40
|
+
--arg event "session_start" \
|
|
41
|
+
--arg source "$SOURCE" \
|
|
42
|
+
--arg model "$MODEL" \
|
|
43
|
+
--arg cwd "$CWD" \
|
|
44
|
+
'{timestamp: $ts, session_id: $sid, event: $event, source: $source, model: $model, cwd: $cwd}')
|
|
45
|
+
;;
|
|
46
|
+
|
|
47
|
+
stop)
|
|
48
|
+
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
|
|
49
|
+
LOG_ENTRY=$(jq -n \
|
|
50
|
+
--arg ts "$TIMESTAMP" \
|
|
51
|
+
--arg sid "$SESSION_ID" \
|
|
52
|
+
--arg event "session_stop" \
|
|
53
|
+
--arg cwd "$CWD" \
|
|
54
|
+
--argjson hook_active "$STOP_HOOK_ACTIVE" \
|
|
55
|
+
'{timestamp: $ts, session_id: $sid, event: $event, cwd: $cwd, stop_hook_active: $hook_active}')
|
|
56
|
+
;;
|
|
57
|
+
|
|
58
|
+
tool-use)
|
|
59
|
+
# Extract tool-specific info
|
|
60
|
+
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
|
|
61
|
+
TOOL_RESPONSE=$(echo "$INPUT" | jq -c '.tool_response // {}')
|
|
62
|
+
TOOL_USE_ID=$(echo "$INPUT" | jq -r '.tool_use_id // ""')
|
|
63
|
+
|
|
64
|
+
# For file operations, extract the path
|
|
65
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // ""')
|
|
66
|
+
COMMAND=$(echo "$TOOL_INPUT" | jq -r '.command // ""')
|
|
67
|
+
|
|
68
|
+
LOG_ENTRY=$(jq -n \
|
|
69
|
+
--arg ts "$TIMESTAMP" \
|
|
70
|
+
--arg sid "$SESSION_ID" \
|
|
71
|
+
--arg event "tool_use" \
|
|
72
|
+
--arg tool "$TOOL_NAME" \
|
|
73
|
+
--arg file "$FILE_PATH" \
|
|
74
|
+
--arg cmd "$COMMAND" \
|
|
75
|
+
--arg cwd "$CWD" \
|
|
76
|
+
--arg mode "$PERMISSION_MODE" \
|
|
77
|
+
'{timestamp: $ts, session_id: $sid, event: $event, tool: $tool, file: $file, command: $cmd, cwd: $cwd, permission_mode: $mode}')
|
|
78
|
+
;;
|
|
79
|
+
|
|
80
|
+
*)
|
|
81
|
+
LOG_ENTRY=$(jq -n \
|
|
82
|
+
--arg ts "$TIMESTAMP" \
|
|
83
|
+
--arg sid "$SESSION_ID" \
|
|
84
|
+
--arg event "$EVENT_TYPE" \
|
|
85
|
+
--arg hook "$HOOK_EVENT" \
|
|
86
|
+
--arg cwd "$CWD" \
|
|
87
|
+
'{timestamp: $ts, session_id: $sid, event: $event, hook_event: $hook, cwd: $cwd}')
|
|
88
|
+
;;
|
|
89
|
+
esac
|
|
90
|
+
|
|
91
|
+
# Append to log files
|
|
92
|
+
echo "$LOG_ENTRY" >> "$LOG_FILE"
|
|
93
|
+
echo "$LOG_ENTRY" >> "$DATE_LOG_FILE"
|
|
94
|
+
|
|
95
|
+
# Success - allow operation to continue
|
|
96
|
+
exit 0
|