start-vibing 1.1.2 → 1.1.3
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/package.json +1 -1
- package/template/.claude/CLAUDE.md +129 -168
- package/template/.claude/README.md +135 -126
- package/template/.claude/agents/analyzer.md +0 -14
- package/template/.claude/agents/commit-manager.md +0 -19
- package/template/.claude/agents/documenter.md +0 -10
- package/template/.claude/agents/domain-updater.md +194 -200
- package/template/.claude/agents/final-validator.md +0 -18
- package/template/.claude/agents/orchestrator.md +0 -12
- package/template/.claude/agents/quality-checker.md +0 -24
- package/template/.claude/agents/research.md +251 -262
- package/template/.claude/agents/security-auditor.md +1 -14
- package/template/.claude/agents/tester.md +0 -8
- package/template/.claude/agents/ui-ux-reviewer.md +0 -8
- package/template/.claude/commands/feature.md +48 -102
- package/template/.claude/config/README.md +30 -30
- package/template/.claude/config/domain-mapping.json +55 -26
- package/template/.claude/config/project-config.json +56 -53
- package/template/.claude/config/quality-gates.json +46 -46
- package/template/.claude/config/security-rules.json +45 -45
- package/template/.claude/config/testing-config.json +168 -168
- package/template/.claude/hooks/SETUP.md +52 -181
- package/template/.claude/hooks/user-prompt-submit.py +37 -246
- package/template/.claude/settings.json +39 -267
- package/template/.claude/skills/codebase-knowledge/SKILL.md +71 -145
- package/template/.claude/skills/codebase-knowledge/domains/claude-system.md +54 -321
- package/template/.claude/skills/docs-tracker/SKILL.md +63 -239
- package/template/.claude/skills/final-check/SKILL.md +72 -284
- package/template/.claude/skills/quality-gate/SKILL.md +71 -278
- package/template/.claude/skills/research-cache/SKILL.md +73 -207
- package/template/.claude/skills/security-scan/SKILL.md +75 -206
- package/template/.claude/skills/test-coverage/SKILL.md +66 -441
- package/template/.claude/skills/ui-ux-audit/SKILL.md +68 -254
- package/template/.claude/hooks/post-tool-use.py +0 -155
- package/template/.claude/hooks/pre-tool-use.py +0 -159
- package/template/.claude/hooks/stop-validation.py +0 -155
- package/template/.claude/hooks/validate-commit.py +0 -200
- package/template/.claude/hooks/workflow-manager.py +0 -350
- package/template/.claude/workflow-state.schema.json +0 -200
|
@@ -1,254 +1,68 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: ui-ux-audit
|
|
3
|
-
description:
|
|
4
|
-
allowed-tools: Read, WebSearch, WebFetch, Grep, Glob
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# UI/UX Audit
|
|
8
|
-
|
|
9
|
-
##
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
"[
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## Accessibility Checklist (WCAG 2.1)
|
|
73
|
-
|
|
74
|
-
### Level A (Required)
|
|
75
|
-
|
|
76
|
-
- [ ] **Text contrast:** Minimum 4.5:1 normal, 3:1 large text
|
|
77
|
-
- [ ] **Alt text:** All images have descriptive alt
|
|
78
|
-
- [ ] **Keyboard navigation:** All interactive elements via Tab
|
|
79
|
-
- [ ] **Visible focus:** Clear outline on focused elements
|
|
80
|
-
- [ ] **Form labels:** All inputs have associated label
|
|
81
|
-
- [ ] **Error messages:** Clear and associated to field
|
|
82
|
-
|
|
83
|
-
### Level AA (Recommended)
|
|
84
|
-
|
|
85
|
-
- [ ] **Resize:** Works up to 200% zoom
|
|
86
|
-
- [ ] **Touch targets:** Minimum 44x44px (`h-11 w-11`)
|
|
87
|
-
- [ ] **Hover/Focus:** Content visible on hover also via focus
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Responsiveness Checklist
|
|
92
|
-
|
|
93
|
-
### Required Viewports
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
const REQUIRED_VIEWPORTS = {
|
|
97
|
-
mobile: { width: 375, height: 667 }, // iPhone SE
|
|
98
|
-
tablet: { width: 768, height: 1024 }, // iPad
|
|
99
|
-
desktop: { width: 1280, height: 800 }, // Laptop
|
|
100
|
-
fullhd: { width: 1920, height: 1080 }, // Common monitor
|
|
101
|
-
};
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Mobile (375px)
|
|
105
|
-
|
|
106
|
-
- [ ] Vertical stack layout (flex-col)
|
|
107
|
-
- [ ] Hamburger menu or bottom nav
|
|
108
|
-
- [ ] Touch-friendly buttons (min 44px)
|
|
109
|
-
- [ ] Readable text without zoom
|
|
110
|
-
- [ ] Vertical scroll only
|
|
111
|
-
|
|
112
|
-
### Desktop (1280px+)
|
|
113
|
-
|
|
114
|
-
- [ ] Use horizontal space
|
|
115
|
-
- [ ] Expanded sidebar
|
|
116
|
-
- [ ] Can increase info density
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## CRITICAL: Zero Horizontal Overflow
|
|
121
|
-
|
|
122
|
-
> **NEVER** should there be horizontal scroll.
|
|
123
|
-
|
|
124
|
-
### Required CSS Patterns
|
|
125
|
-
|
|
126
|
-
```tsx
|
|
127
|
-
// Main layout
|
|
128
|
-
<div className="flex h-screen w-screen overflow-hidden">
|
|
129
|
-
{/* Sidebar */}
|
|
130
|
-
<aside className="w-[260px] flex-shrink-0 overflow-y-auto">...</aside>
|
|
131
|
-
|
|
132
|
-
{/* Main content */}
|
|
133
|
-
<main className="flex-1 min-w-0 overflow-hidden">...</main>
|
|
134
|
-
</div>
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
### Overflow Checklist
|
|
138
|
-
|
|
139
|
-
- [ ] Main container has `overflow-hidden`?
|
|
140
|
-
- [ ] Sidebar has `flex-shrink-0`?
|
|
141
|
-
- [ ] Main content has `min-w-0`?
|
|
142
|
-
- [ ] No element with width larger than container?
|
|
143
|
-
- [ ] Tested at 1920x1080?
|
|
144
|
-
|
|
145
|
-
---
|
|
146
|
-
|
|
147
|
-
## Skeleton Loading
|
|
148
|
-
|
|
149
|
-
### Rule: Every Component HAS Skeleton
|
|
150
|
-
|
|
151
|
-
- [ ] Skeleton created with component
|
|
152
|
-
- [ ] Same dimensions as final content
|
|
153
|
-
- [ ] `animate-pulse` animation
|
|
154
|
-
- [ ] Same visual structure
|
|
155
|
-
|
|
156
|
-
### Structure
|
|
157
|
-
|
|
158
|
-
```
|
|
159
|
-
components/features/UserCard/
|
|
160
|
-
├── UserCard.tsx
|
|
161
|
-
└── UserCardSkeleton.tsx
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### Template
|
|
165
|
-
|
|
166
|
-
```tsx
|
|
167
|
-
export function ComponentSkeleton() {
|
|
168
|
-
return (
|
|
169
|
-
<div className="animate-pulse">
|
|
170
|
-
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2" />
|
|
171
|
-
<div className="h-4 bg-gray-200 rounded w-1/2" />
|
|
172
|
-
</div>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## Design System (Tailwind)
|
|
180
|
-
|
|
181
|
-
### Colors
|
|
182
|
-
|
|
183
|
-
- **Primary:** `indigo-600` (buttons, links)
|
|
184
|
-
- **Secondary:** `gray-600` (secondary text)
|
|
185
|
-
- **Success:** `green-600`
|
|
186
|
-
- **Error:** `red-600`
|
|
187
|
-
- **Warning:** `yellow-600`
|
|
188
|
-
|
|
189
|
-
### Spacing (4px scale)
|
|
190
|
-
|
|
191
|
-
```
|
|
192
|
-
p-1: 4px p-2: 8px p-4: 16px
|
|
193
|
-
p-6: 24px p-8: 32px p-12: 48px
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
### Border & Shadow
|
|
197
|
-
|
|
198
|
-
- **Border radius:** `rounded-lg` (8px) default
|
|
199
|
-
- **Shadows:** `shadow-sm` for cards, `shadow-lg` for modals
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
## Output Format
|
|
204
|
-
|
|
205
|
-
```markdown
|
|
206
|
-
## UI/UX AUDIT - Report
|
|
207
|
-
|
|
208
|
-
### Feature
|
|
209
|
-
|
|
210
|
-
- **Name:** [name]
|
|
211
|
-
- **Type:** [form/dashboard/list/etc]
|
|
212
|
-
- **Approach:** [mobile-first/desktop-first]
|
|
213
|
-
|
|
214
|
-
### Competitor Research
|
|
215
|
-
|
|
216
|
-
- [x] Researched 5 competitors
|
|
217
|
-
- **Pattern identified:** [description]
|
|
218
|
-
|
|
219
|
-
### Accessibility
|
|
220
|
-
|
|
221
|
-
- [x] Contrast validated (4.5:1+)
|
|
222
|
-
- [x] Touch targets 44x44px
|
|
223
|
-
- [x] Alt text on images
|
|
224
|
-
- [x] Labels on forms
|
|
225
|
-
|
|
226
|
-
### Responsiveness
|
|
227
|
-
|
|
228
|
-
- [x] Mobile (375px) - OK
|
|
229
|
-
- [x] Tablet (768px) - OK
|
|
230
|
-
- [x] Desktop (1280px) - OK
|
|
231
|
-
- [x] FullHD (1920px) - OK
|
|
232
|
-
- [x] Zero horizontal overflow
|
|
233
|
-
|
|
234
|
-
### Skeleton
|
|
235
|
-
|
|
236
|
-
- [x] Created
|
|
237
|
-
- [x] Correct dimensions
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
---
|
|
241
|
-
|
|
242
|
-
## Critical Rules
|
|
243
|
-
|
|
244
|
-
1. **ALWAYS research competitors** - Before implementing UI
|
|
245
|
-
2. **ALWAYS validate accessibility** - WCAG 2.1 Level AA
|
|
246
|
-
3. **ALWAYS test viewports** - Mobile, tablet, desktop, fullhd
|
|
247
|
-
4. **NEVER horizontal overflow** - Check all viewports
|
|
248
|
-
5. **ALWAYS create skeleton** - With the component
|
|
249
|
-
|
|
250
|
-
---
|
|
251
|
-
|
|
252
|
-
## Version
|
|
253
|
-
|
|
254
|
-
- **v2.0.0** - Generic template
|
|
1
|
+
---
|
|
2
|
+
name: ui-ux-audit
|
|
3
|
+
description: Audits UI/UX for accessibility and design. Activates when implementing UI components, pages, layouts, forms, or when user mentions 'design', 'responsive', 'accessibility', 'mobile', 'component', or 'UI'.
|
|
4
|
+
allowed-tools: Read, WebSearch, WebFetch, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# UI/UX Audit
|
|
8
|
+
|
|
9
|
+
## When to Use
|
|
10
|
+
|
|
11
|
+
- Before implementing UI features
|
|
12
|
+
- When creating new components/pages
|
|
13
|
+
- When user asks about design patterns
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. **Research** - competitors, patterns
|
|
18
|
+
2. **Implement** - follow accessibility checklist
|
|
19
|
+
3. **Validate** - test all viewports
|
|
20
|
+
|
|
21
|
+
## Accessibility Checklist (WCAG 2.1)
|
|
22
|
+
|
|
23
|
+
- [ ] Text contrast: 4.5:1 minimum
|
|
24
|
+
- [ ] Touch targets: 44x44px minimum
|
|
25
|
+
- [ ] Keyboard navigation: all elements via Tab
|
|
26
|
+
- [ ] Alt text: all images
|
|
27
|
+
- [ ] Form labels: all inputs have labels
|
|
28
|
+
- [ ] Visible focus: clear outline
|
|
29
|
+
|
|
30
|
+
## Viewports (Required)
|
|
31
|
+
|
|
32
|
+
| Name | Size | Check |
|
|
33
|
+
|------|------|-------|
|
|
34
|
+
| Mobile | 375x667 | Hamburger menu, vertical stack |
|
|
35
|
+
| Tablet | 768x1024 | Hybrid layout |
|
|
36
|
+
| Desktop | 1280x800 | Sidebar, horizontal space |
|
|
37
|
+
|
|
38
|
+
## Critical: Zero Horizontal Overflow
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
<div className="flex h-screen w-screen overflow-hidden">
|
|
42
|
+
<aside className="w-[260px] flex-shrink-0">...</aside>
|
|
43
|
+
<main className="flex-1 min-w-0 overflow-hidden">...</main>
|
|
44
|
+
</div>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Skeleton Loading
|
|
48
|
+
|
|
49
|
+
Every async component needs a skeleton:
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
export function ComponentSkeleton() {
|
|
53
|
+
return (
|
|
54
|
+
<div className="animate-pulse">
|
|
55
|
+
<div className="h-4 bg-gray-200 rounded w-3/4" />
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Rules
|
|
62
|
+
|
|
63
|
+
1. **Research competitors** before implementing
|
|
64
|
+
2. **Test all viewports** before committing
|
|
65
|
+
3. **Never horizontal overflow**
|
|
66
|
+
4. **Create skeleton** with component
|
|
67
|
+
|
|
68
|
+
See `templates/` for audit report templates.
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
PostToolUse Hook - Track File Modifications
|
|
4
|
-
|
|
5
|
-
This hook records all file modifications to workflow-state.json
|
|
6
|
-
for later validation by Husky pre-commit hook.
|
|
7
|
-
|
|
8
|
-
Exit codes:
|
|
9
|
-
- 0: Always (tracking is non-blocking)
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import sys
|
|
14
|
-
import os
|
|
15
|
-
from datetime import datetime
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
# Get project directory
|
|
19
|
-
PROJECT_DIR = os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd())
|
|
20
|
-
WORKFLOW_STATE_PATH = Path(PROJECT_DIR) / '.claude' / 'workflow-state.json'
|
|
21
|
-
|
|
22
|
-
# Tools that modify files
|
|
23
|
-
FILE_MODIFY_TOOLS = {'Edit', 'Write', 'NotebookEdit'}
|
|
24
|
-
FILE_DELETE_TOOLS = {'Bash'} # rm commands
|
|
25
|
-
|
|
26
|
-
# Test file patterns
|
|
27
|
-
TEST_PATTERNS = ['test', 'spec', '.test.', '.spec.', '__tests__']
|
|
28
|
-
|
|
29
|
-
# Doc file patterns
|
|
30
|
-
DOC_PATTERNS = ['docs/', 'README', 'CHANGELOG', '.md', 'domains/']
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def load_workflow_state():
|
|
34
|
-
"""Load current workflow state"""
|
|
35
|
-
if not WORKFLOW_STATE_PATH.exists():
|
|
36
|
-
return {"version": "1.0.0", "currentTask": None, "sessions": []}
|
|
37
|
-
try:
|
|
38
|
-
with open(WORKFLOW_STATE_PATH) as f:
|
|
39
|
-
return json.load(f)
|
|
40
|
-
except (json.JSONDecodeError, IOError):
|
|
41
|
-
return {"version": "1.0.0", "currentTask": None, "sessions": []}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def save_workflow_state(state: dict):
|
|
45
|
-
"""Save workflow state"""
|
|
46
|
-
WORKFLOW_STATE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
-
with open(WORKFLOW_STATE_PATH, 'w') as f:
|
|
48
|
-
json.dump(state, f, indent=2)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def is_test_file(path: str) -> bool:
|
|
52
|
-
"""Check if file is a test file"""
|
|
53
|
-
return any(p in path.lower() for p in TEST_PATTERNS)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def is_doc_file(path: str) -> bool:
|
|
57
|
-
"""Check if file is a documentation file"""
|
|
58
|
-
return any(p in path.lower() for p in DOC_PATTERNS)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def main():
|
|
62
|
-
# Read hook input from stdin
|
|
63
|
-
try:
|
|
64
|
-
hook_input = json.load(sys.stdin)
|
|
65
|
-
except json.JSONDecodeError:
|
|
66
|
-
sys.exit(0)
|
|
67
|
-
|
|
68
|
-
tool_name = hook_input.get('tool_name', '')
|
|
69
|
-
tool_input = hook_input.get('tool_input', {})
|
|
70
|
-
tool_result = hook_input.get('tool_result', {})
|
|
71
|
-
|
|
72
|
-
# Only track file modification tools
|
|
73
|
-
if tool_name not in FILE_MODIFY_TOOLS:
|
|
74
|
-
sys.exit(0)
|
|
75
|
-
|
|
76
|
-
# Get file path
|
|
77
|
-
file_path = tool_input.get('file_path', tool_input.get('notebook_path', ''))
|
|
78
|
-
|
|
79
|
-
if not file_path:
|
|
80
|
-
sys.exit(0)
|
|
81
|
-
|
|
82
|
-
# Skip workflow state file itself
|
|
83
|
-
if 'workflow-state.json' in file_path:
|
|
84
|
-
sys.exit(0)
|
|
85
|
-
|
|
86
|
-
# Load state
|
|
87
|
-
state = load_workflow_state()
|
|
88
|
-
|
|
89
|
-
# If no current task, just exit (pre-tool-use should have blocked)
|
|
90
|
-
if not state.get('currentTask'):
|
|
91
|
-
sys.exit(0)
|
|
92
|
-
|
|
93
|
-
task = state['currentTask']
|
|
94
|
-
|
|
95
|
-
# Determine action type
|
|
96
|
-
# For Write tool, check if file existed
|
|
97
|
-
action = 'modified'
|
|
98
|
-
if tool_name == 'Write':
|
|
99
|
-
# Assume created if not in modified list
|
|
100
|
-
existing = [m['path'] for m in task.get('modifiedFiles', [])]
|
|
101
|
-
if file_path not in existing:
|
|
102
|
-
action = 'created'
|
|
103
|
-
|
|
104
|
-
# Normalize path
|
|
105
|
-
file_path = file_path.replace('\\', '/')
|
|
106
|
-
|
|
107
|
-
# Check if file was approved
|
|
108
|
-
approved_files = task.get('approvedFiles', [])
|
|
109
|
-
was_approved = any(
|
|
110
|
-
file_path.endswith(af) or file_path == af or
|
|
111
|
-
(af.endswith('*') and file_path.startswith(af[:-1]))
|
|
112
|
-
for af in approved_files
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
# Record modification
|
|
116
|
-
modification = {
|
|
117
|
-
'path': file_path,
|
|
118
|
-
'action': action,
|
|
119
|
-
'timestamp': datetime.now().isoformat(),
|
|
120
|
-
'approved': was_approved
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if 'modifiedFiles' not in task:
|
|
124
|
-
task['modifiedFiles'] = []
|
|
125
|
-
|
|
126
|
-
# Update or add
|
|
127
|
-
existing_paths = [m['path'] for m in task['modifiedFiles']]
|
|
128
|
-
if file_path in existing_paths:
|
|
129
|
-
idx = existing_paths.index(file_path)
|
|
130
|
-
task['modifiedFiles'][idx] = modification
|
|
131
|
-
else:
|
|
132
|
-
task['modifiedFiles'].append(modification)
|
|
133
|
-
|
|
134
|
-
# Track test files
|
|
135
|
-
if is_test_file(file_path):
|
|
136
|
-
if 'testsCreated' not in task:
|
|
137
|
-
task['testsCreated'] = []
|
|
138
|
-
if file_path not in task['testsCreated']:
|
|
139
|
-
task['testsCreated'].append(file_path)
|
|
140
|
-
|
|
141
|
-
# Track doc files
|
|
142
|
-
if is_doc_file(file_path):
|
|
143
|
-
if 'docsUpdated' not in task:
|
|
144
|
-
task['docsUpdated'] = []
|
|
145
|
-
if file_path not in task['docsUpdated']:
|
|
146
|
-
task['docsUpdated'].append(file_path)
|
|
147
|
-
|
|
148
|
-
# Save state
|
|
149
|
-
save_workflow_state(state)
|
|
150
|
-
|
|
151
|
-
sys.exit(0)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if __name__ == '__main__':
|
|
155
|
-
main()
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
PreToolUse Hook - File Modification Enforcement
|
|
4
|
-
|
|
5
|
-
This hook blocks file modifications unless:
|
|
6
|
-
1. The analyzer agent has approved the file
|
|
7
|
-
2. The file is in the approvedFiles list in workflow-state.json
|
|
8
|
-
|
|
9
|
-
Exit codes:
|
|
10
|
-
- 0: Allow the operation
|
|
11
|
-
- 2: Block the operation (shows stderr to Claude)
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
import json
|
|
15
|
-
import sys
|
|
16
|
-
import os
|
|
17
|
-
from datetime import datetime
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
# Get project directory
|
|
21
|
-
PROJECT_DIR = os.environ.get('CLAUDE_PROJECT_DIR', os.getcwd())
|
|
22
|
-
WORKFLOW_STATE_PATH = Path(PROJECT_DIR) / '.claude' / 'workflow-state.json'
|
|
23
|
-
|
|
24
|
-
# Tools that modify files
|
|
25
|
-
FILE_MODIFY_TOOLS = {'Edit', 'Write', 'NotebookEdit'}
|
|
26
|
-
|
|
27
|
-
# Files always allowed (config, etc)
|
|
28
|
-
ALWAYS_ALLOWED = {
|
|
29
|
-
'.claude/workflow-state.json',
|
|
30
|
-
'.claude/hooks/',
|
|
31
|
-
'package-lock.json',
|
|
32
|
-
'node_modules/',
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
# Files always blocked
|
|
36
|
-
ALWAYS_BLOCKED = {
|
|
37
|
-
'.env',
|
|
38
|
-
'.env.local',
|
|
39
|
-
'.env.production',
|
|
40
|
-
'credentials.json',
|
|
41
|
-
'secrets/',
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def load_workflow_state():
|
|
46
|
-
"""Load current workflow state"""
|
|
47
|
-
if not WORKFLOW_STATE_PATH.exists():
|
|
48
|
-
return None
|
|
49
|
-
try:
|
|
50
|
-
with open(WORKFLOW_STATE_PATH) as f:
|
|
51
|
-
return json.load(f)
|
|
52
|
-
except (json.JSONDecodeError, IOError):
|
|
53
|
-
return None
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def is_file_approved(file_path: str, state: dict) -> tuple[bool, str]:
|
|
57
|
-
"""Check if file is approved for modification"""
|
|
58
|
-
|
|
59
|
-
# Normalize path
|
|
60
|
-
file_path = file_path.replace('\\', '/')
|
|
61
|
-
|
|
62
|
-
# Always blocked files
|
|
63
|
-
for blocked in ALWAYS_BLOCKED:
|
|
64
|
-
if blocked in file_path:
|
|
65
|
-
return False, f"BLOCKED: {file_path} is in the always-blocked list (sensitive file)"
|
|
66
|
-
|
|
67
|
-
# Always allowed files
|
|
68
|
-
for allowed in ALWAYS_ALLOWED:
|
|
69
|
-
if allowed in file_path:
|
|
70
|
-
return True, "Allowed (system file)"
|
|
71
|
-
|
|
72
|
-
# No active task - block all modifications
|
|
73
|
-
if not state or not state.get('currentTask'):
|
|
74
|
-
return False, f"""BLOCKED: No active task in workflow-state.json
|
|
75
|
-
|
|
76
|
-
To modify files, you must first:
|
|
77
|
-
1. Start a task via orchestrator agent
|
|
78
|
-
2. Run analyzer agent to approve files for modification
|
|
79
|
-
3. The file '{file_path}' must be in the approvedFiles list
|
|
80
|
-
|
|
81
|
-
Current state: No task active. Start with orchestrator first."""
|
|
82
|
-
|
|
83
|
-
task = state['currentTask']
|
|
84
|
-
approved_files = task.get('approvedFiles', [])
|
|
85
|
-
|
|
86
|
-
# Check if analyzer has run
|
|
87
|
-
agents = task.get('agents', {})
|
|
88
|
-
analyzer = agents.get('analyzer', {})
|
|
89
|
-
|
|
90
|
-
if not analyzer.get('executed'):
|
|
91
|
-
return False, f"""BLOCKED: Analyzer agent has not executed yet
|
|
92
|
-
|
|
93
|
-
To modify '{file_path}', you must:
|
|
94
|
-
1. Run the analyzer agent first
|
|
95
|
-
2. Analyzer will determine which files can be modified
|
|
96
|
-
3. Add approved files to workflow-state.json
|
|
97
|
-
|
|
98
|
-
Current task: {task.get('description', 'Unknown')}
|
|
99
|
-
Analyzer status: Not executed"""
|
|
100
|
-
|
|
101
|
-
# Check if file is in approved list
|
|
102
|
-
for approved in approved_files:
|
|
103
|
-
# Support glob patterns
|
|
104
|
-
if approved.endswith('*'):
|
|
105
|
-
if file_path.startswith(approved[:-1]):
|
|
106
|
-
return True, f"Approved (matches pattern: {approved})"
|
|
107
|
-
elif approved == file_path or file_path.endswith(approved):
|
|
108
|
-
return True, f"Approved by analyzer"
|
|
109
|
-
|
|
110
|
-
return False, f"""BLOCKED: File not in approved list
|
|
111
|
-
|
|
112
|
-
File: {file_path}
|
|
113
|
-
Task: {task.get('description', 'Unknown')}
|
|
114
|
-
|
|
115
|
-
Approved files for this task:
|
|
116
|
-
{chr(10).join(f' - {f}' for f in approved_files) if approved_files else ' (none)'}
|
|
117
|
-
|
|
118
|
-
To modify this file, run analyzer again and add it to approvedFiles."""
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def main():
|
|
122
|
-
# Read hook input from stdin
|
|
123
|
-
try:
|
|
124
|
-
hook_input = json.load(sys.stdin)
|
|
125
|
-
except json.JSONDecodeError:
|
|
126
|
-
# Can't parse input, allow by default
|
|
127
|
-
sys.exit(0)
|
|
128
|
-
|
|
129
|
-
tool_name = hook_input.get('tool_name', '')
|
|
130
|
-
tool_input = hook_input.get('tool_input', {})
|
|
131
|
-
|
|
132
|
-
# Only check file modification tools
|
|
133
|
-
if tool_name not in FILE_MODIFY_TOOLS:
|
|
134
|
-
sys.exit(0)
|
|
135
|
-
|
|
136
|
-
# Get file path from tool input
|
|
137
|
-
file_path = tool_input.get('file_path', tool_input.get('notebook_path', ''))
|
|
138
|
-
|
|
139
|
-
if not file_path:
|
|
140
|
-
sys.exit(0)
|
|
141
|
-
|
|
142
|
-
# Load workflow state
|
|
143
|
-
state = load_workflow_state()
|
|
144
|
-
|
|
145
|
-
# Check if file is approved
|
|
146
|
-
approved, message = is_file_approved(file_path, state)
|
|
147
|
-
|
|
148
|
-
if approved:
|
|
149
|
-
# Log the approval (optional verbose output)
|
|
150
|
-
# print(f"✓ {message}", file=sys.stderr)
|
|
151
|
-
sys.exit(0)
|
|
152
|
-
else:
|
|
153
|
-
# Block with error message
|
|
154
|
-
print(message, file=sys.stderr)
|
|
155
|
-
sys.exit(2)
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if __name__ == '__main__':
|
|
159
|
-
main()
|