claudenv 1.0.2 → 1.2.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.
@@ -0,0 +1,279 @@
1
+ // =============================================
2
+ // Hook & config file generation for autonomy profiles
3
+ // =============================================
4
+
5
+ import { CREDENTIAL_PATHS } from './profiles.js';
6
+
7
+ /**
8
+ * Generate .claude/settings.json content for a given profile.
9
+ * @param {object} profile - Autonomy profile
10
+ * @param {object} detected - Tech stack detection result
11
+ * @returns {string} JSON string
12
+ */
13
+ export function generateSettingsJson(profile, detected = {}) {
14
+ const settings = {};
15
+
16
+ if (profile.allowedTools && profile.allowedTools.length > 0) {
17
+ settings.permissions = settings.permissions || {};
18
+ settings.permissions.allow = profile.allowedTools;
19
+ }
20
+
21
+ if (profile.disallowedTools && profile.disallowedTools.length > 0) {
22
+ settings.permissions = settings.permissions || {};
23
+ settings.permissions.deny = profile.disallowedTools;
24
+ }
25
+
26
+ // Add stack-aware package manager restrictions for moderate/safe
27
+ if (detected.packageManager && !profile.skipPermissions) {
28
+ settings.permissions = settings.permissions || {};
29
+ settings.permissions.deny = settings.permissions.deny || [];
30
+ const wrongManagers = getWrongPackageManagers(detected.packageManager);
31
+ for (const cmd of wrongManagers) {
32
+ if (!settings.permissions.deny.includes(cmd)) {
33
+ settings.permissions.deny.push(cmd);
34
+ }
35
+ }
36
+ }
37
+
38
+ // Hooks config — PascalCase keys, command objects per Claude Code format
39
+ const preToolUseHook = {
40
+ type: 'command',
41
+ command: 'bash .claude/hooks/pre-tool-use.sh',
42
+ timeout: 10,
43
+ };
44
+ const auditLogHook = {
45
+ type: 'command',
46
+ command: 'bash .claude/hooks/audit-log.sh',
47
+ timeout: 10,
48
+ };
49
+
50
+ settings.hooks = {
51
+ PreToolUse: [
52
+ {
53
+ matcher: 'Bash',
54
+ hooks: [preToolUseHook],
55
+ },
56
+ {
57
+ matcher: 'Edit',
58
+ hooks: [preToolUseHook],
59
+ },
60
+ {
61
+ matcher: 'Write',
62
+ hooks: [preToolUseHook],
63
+ },
64
+ {
65
+ matcher: 'Read',
66
+ hooks: [preToolUseHook],
67
+ },
68
+ ],
69
+ PostToolUse: [
70
+ {
71
+ matcher: '',
72
+ hooks: [auditLogHook],
73
+ },
74
+ ],
75
+ };
76
+
77
+ settings.enableAllProjectMcpServers = false;
78
+
79
+ return JSON.stringify(settings, null, 2) + '\n';
80
+ }
81
+
82
+ /**
83
+ * Generate pre-tool-use hook script.
84
+ * @param {object} profile - Autonomy profile
85
+ * @param {object} detected - Tech stack detection result
86
+ * @returns {string} Bash script
87
+ */
88
+ export function generatePreToolUseHook(profile, detected = {}) {
89
+ const credentialPathsStr = CREDENTIAL_PATHS.map((p) => ` "${p}"`).join('\n');
90
+ const wrongManagerBlock = buildWrongManagerBlock(detected.packageManager);
91
+
92
+ const credentialAction =
93
+ profile.credentialPolicy === 'block'
94
+ ? ` echo "BLOCKED: Access to credential path: $input_path" >&2
95
+ exit 2`
96
+ : ` echo "WARNING: Accessing credential path: $input_path" >&2
97
+ # Log warning to audit
98
+ echo "{\\"ts\\":\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\",\\"event\\":\\"credential_warning\\",\\"path\\":\\"$input_path\\"}" >> .claude/audit-log.jsonl
99
+ exit 0`;
100
+
101
+ return `#!/usr/bin/env bash
102
+ # Pre-tool-use hook — generated by claudenv autonomy (profile: ${profile.name})
103
+ # Exit 0 = allow, Exit 2 = block
104
+
105
+ set -euo pipefail
106
+
107
+ TOOL_NAME="\${CLAUDE_TOOL_NAME:-}"
108
+ TOOL_INPUT="\${CLAUDE_TOOL_INPUT:-}"
109
+
110
+ # === Hard blocks (all profiles) ===
111
+
112
+ # Block rm -rf / rm -fr
113
+ if echo "$TOOL_INPUT" | grep -qE 'rm\\s+.*-[rR].*-[fF]|rm\\s+.*-[fF].*-[rR]|rm\\s+-rf\\b|rm\\s+-fr\\b'; then
114
+ echo "BLOCKED: Destructive rm command detected" >&2
115
+ exit 2
116
+ fi
117
+
118
+ # Block force push to main/master
119
+ if echo "$TOOL_INPUT" | grep -qE 'git\\s+push\\s+(--force|-f)\\s.*(main|master)|git\\s+push\\s+.*\\s(main|master)\\s+(--force|-f)'; then
120
+ echo "BLOCKED: Force push to main/master" >&2
121
+ exit 2
122
+ fi
123
+
124
+ # Block sudo
125
+ if echo "$TOOL_INPUT" | grep -qE '^sudo\\s|\\bsudo\\s'; then
126
+ echo "BLOCKED: sudo command" >&2
127
+ exit 2
128
+ fi
129
+
130
+ # === Credential path checks ===
131
+ CREDENTIAL_PATHS=(
132
+ ${credentialPathsStr}
133
+ )
134
+
135
+ # Expand ~ to HOME
136
+ input_path=$(echo "$TOOL_INPUT" | grep -oE '~/[^ "]+|\\$HOME/[^ "]+' | head -1 || true)
137
+ if [ -n "$input_path" ]; then
138
+ for cred_path in "\${CREDENTIAL_PATHS[@]}"; do
139
+ expanded_cred="\${cred_path/#\\~/$HOME}"
140
+ expanded_input="\${input_path/#\\~/$HOME}"
141
+ if [[ "$expanded_input" == "$expanded_cred"* ]]; then
142
+ ${credentialAction}
143
+ fi
144
+ done
145
+ fi
146
+ ${wrongManagerBlock}
147
+ # All checks passed
148
+ exit 0
149
+ `;
150
+ }
151
+
152
+ /**
153
+ * Generate audit log hook script.
154
+ * @returns {string} Bash script
155
+ */
156
+ export function generateAuditLogHook() {
157
+ return `#!/usr/bin/env bash
158
+ # Post-tool-use audit hook — generated by claudenv autonomy
159
+ # Logs every tool invocation to .claude/audit-log.jsonl
160
+
161
+ set -uo pipefail
162
+
163
+ TOOL_NAME="\${CLAUDE_TOOL_NAME:-unknown}"
164
+ TOOL_INPUT="\${CLAUDE_TOOL_INPUT:-}"
165
+ SESSION_ID="\${CLAUDE_SESSION_ID:-}"
166
+
167
+ # Truncate input for logging (max 500 chars)
168
+ TRUNCATED_INPUT=$(echo "$TOOL_INPUT" | head -c 500)
169
+
170
+ # Escape for JSON
171
+ ESCAPED_INPUT=$(echo "$TRUNCATED_INPUT" | sed 's/\\\\/\\\\\\\\/g; s/"/\\\\"/g; s/\\t/\\\\t/g' | tr '\\n' ' ')
172
+
173
+ mkdir -p .claude
174
+
175
+ echo "{\\"ts\\":\\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\\",\\"tool\\":\\"$TOOL_NAME\\",\\"input\\":\\"$ESCAPED_INPUT\\",\\"session\\":\\"$SESSION_ID\\"}" >> .claude/audit-log.jsonl
176
+
177
+ exit 0
178
+ `;
179
+ }
180
+
181
+ /**
182
+ * Generate shell aliases.
183
+ * @param {object} profile - Autonomy profile
184
+ * @returns {string} Shell script with aliases
185
+ */
186
+ export function generateAliases(profile) {
187
+ return `#!/usr/bin/env bash
188
+ # Shell aliases — generated by claudenv autonomy (profile: ${profile.name})
189
+ # Source this file: source .claude/aliases.sh
190
+
191
+ # Safe mode — read-only, plan mode
192
+ alias claude-safe='claude --allowedTools "Read,Glob,Grep,Bash(ls *),Bash(cat *),Bash(git status),Bash(git log *),Bash(git diff *)"'
193
+
194
+ # Full autonomy — skip all permission prompts
195
+ alias claude-yolo='claude --dangerously-skip-permissions'
196
+
197
+ # CI mode — headless, JSON output, budget-limited
198
+ alias claude-ci='claude -p --output-format json --dangerously-skip-permissions --max-turns 50'
199
+
200
+ # Local dev — current profile (${profile.name})
201
+ alias claude-local='claude${profile.skipPermissions ? ' --dangerously-skip-permissions' : ''}${profile.disallowedTools && profile.disallowedTools.length > 0 ? ` --disallowedTools "${profile.disallowedTools.join(',')}"` : ''}'
202
+ `;
203
+ }
204
+
205
+ /**
206
+ * Generate GitHub Actions workflow for CI profile.
207
+ * @returns {string} YAML content
208
+ */
209
+ export function generateCIWorkflow() {
210
+ return `# Claude CI workflow — generated by claudenv autonomy (profile: ci)
211
+ name: Claude CI
212
+
213
+ on:
214
+ issues:
215
+ types: [opened, labeled]
216
+ pull_request_target:
217
+ types: [opened, synchronize]
218
+
219
+ jobs:
220
+ claude:
221
+ if: |
222
+ (github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'claude')) ||
223
+ github.event_name == 'pull_request_target'
224
+ runs-on: ubuntu-latest
225
+ permissions:
226
+ contents: read
227
+ pull-requests: write
228
+ issues: write
229
+
230
+ steps:
231
+ - name: Checkout
232
+ uses: actions/checkout@v4
233
+ with:
234
+ fetch-depth: 1
235
+
236
+ - name: Run Claude
237
+ uses: anthropics/claude-code-action@v1
238
+ with:
239
+ anthropic_api_key: \${{ secrets.ANTHROPIC_API_KEY }}
240
+ max_turns: "50"
241
+ timeout_minutes: "30"
242
+ `;
243
+ }
244
+
245
+ // =============================================
246
+ // Internal helpers
247
+ // =============================================
248
+
249
+ function getWrongPackageManagers(detected) {
250
+ const managers = {
251
+ npm: ['Bash(yarn *)', 'Bash(pnpm *)', 'Bash(bun *)'],
252
+ yarn: ['Bash(npm install *)', 'Bash(npm ci *)', 'Bash(pnpm *)', 'Bash(bun *)'],
253
+ pnpm: ['Bash(npm install *)', 'Bash(npm ci *)', 'Bash(yarn *)', 'Bash(bun *)'],
254
+ bun: ['Bash(npm install *)', 'Bash(npm ci *)', 'Bash(yarn *)', 'Bash(pnpm *)'],
255
+ };
256
+ return managers[detected] || [];
257
+ }
258
+
259
+ function buildWrongManagerBlock(packageManager) {
260
+ if (!packageManager) return '';
261
+ const wrong = getWrongPackageManagers(packageManager);
262
+ if (wrong.length === 0) return '';
263
+
264
+ const patterns = wrong
265
+ .map((t) => t.replace('Bash(', '').replace(')', '').replace(' *', ''))
266
+ .map((p) => `"${p}"`)
267
+ .join(' ');
268
+
269
+ return `
270
+ # === Wrong package manager check ===
271
+ WRONG_MANAGERS=(${patterns})
272
+ for mgr in "\${WRONG_MANAGERS[@]}"; do
273
+ if echo "$TOOL_INPUT" | grep -qE "^$mgr\\b"; then
274
+ echo "BLOCKED: Use ${packageManager} instead of $mgr in this project" >&2
275
+ exit 2
276
+ fi
277
+ done
278
+ `;
279
+ }
package/src/index.js CHANGED
@@ -3,3 +3,7 @@ export { generateDocs, writeDocs } from './generator.js';
3
3
  export { validateClaudeMd, validateStructure, crossReferenceCheck } from './validator.js';
4
4
  export { runExistingProjectFlow, runColdStartFlow, buildDefaultConfig } from './prompts.js';
5
5
  export { installScaffold } from './generator.js';
6
+ export { runLoop, spawnClaude, checkClaudeCli } from './loop.js';
7
+ export { AUTONOMY_PROFILES, getProfile, listProfiles, CREDENTIAL_PATHS } from './profiles.js';
8
+ export { generateSettingsJson, generatePreToolUseHook, generateAuditLogHook, generateAliases, generateCIWorkflow } from './hooks-gen.js';
9
+ export { generateAutonomyConfig, printSecuritySummary, getFullModeWarning } from './autonomy.js';
package/src/installer.js CHANGED
@@ -92,6 +92,8 @@ export async function uninstallGlobal(options = {}) {
92
92
 
93
93
  const targets = [
94
94
  join(targetBase, 'commands', 'claudenv.md'),
95
+ join(targetBase, 'commands', 'setup-mcp.md'),
96
+ join(targetBase, 'commands', 'improve.md'),
95
97
  join(targetBase, 'skills', 'claudenv'),
96
98
  ];
97
99