claudex-setup 1.15.0 → 1.15.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudex-setup",
3
- "version": "1.15.0",
3
+ "version": "1.15.1",
4
4
  "description": "Score your repo's Claude Code setup against 84 checks. See gaps, apply fixes selectively with rollback, govern hooks and permissions, and benchmark impact — without breaking existing config.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/governance.js CHANGED
@@ -254,36 +254,46 @@ function buildHookConfig(hookFiles, profileKey) {
254
254
  return {};
255
255
  }
256
256
 
257
+ // Detect hook runtime: .js files use node, .sh files use bash
258
+ const hookCommand = (file) => {
259
+ if (file.endsWith('.js')) return `node .claude/hooks/${file}`;
260
+ return `bash .claude/hooks/${file}`;
261
+ };
262
+ const isSecrets = (f) => f === 'protect-secrets.sh' || f === 'protect-secrets.js';
263
+ const isSession = (f) => f === 'session-start.sh' || f === 'session-start.js';
264
+
257
265
  const hookConfig = {
258
266
  PostToolUse: [{
259
267
  matcher: 'Write|Edit',
260
268
  hooks: uniqueFiles
261
- .filter(file => file !== 'protect-secrets.sh' && file !== 'session-start.sh')
269
+ .filter(file => !isSecrets(file) && !isSession(file))
262
270
  .map(file => ({
263
271
  type: 'command',
264
- command: `bash .claude/hooks/${file}`,
272
+ command: hookCommand(file),
265
273
  timeout: 10,
266
274
  })),
267
275
  }],
268
276
  };
269
277
 
270
- if (uniqueFiles.includes('protect-secrets.sh')) {
278
+ const secretsFile = uniqueFiles.find(isSecrets);
279
+ if (secretsFile) {
271
280
  hookConfig.PreToolUse = [{
272
281
  matcher: 'Read|Write|Edit',
273
282
  hooks: [{
274
283
  type: 'command',
275
- command: 'bash .claude/hooks/protect-secrets.sh',
284
+ command: hookCommand(secretsFile),
276
285
  timeout: 5,
277
286
  }],
278
287
  }];
279
288
  }
280
289
 
281
- if (uniqueFiles.includes('session-start.sh')) {
290
+ const sessionFile = uniqueFiles.find(isSession);
291
+ if (sessionFile) {
282
292
  hookConfig.SessionStart = [{
283
293
  matcher: '*',
284
294
  hooks: [{
285
295
  type: 'command',
286
- command: 'bash .claude/hooks/session-start.sh',
296
+ command: hookCommand(sessionFile),
287
297
  timeout: 5,
288
298
  }],
289
299
  }];
package/src/plans.js CHANGED
@@ -260,7 +260,7 @@ function buildAgentPatchFiles(ctx) {
260
260
 
261
261
  function buildHookSettings(ctx, plannedHookFiles, options = {}) {
262
262
  const existing = ctx.hasDir('.claude/hooks')
263
- ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
263
+ ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh') || file.endsWith('.js'))
264
264
  : [];
265
265
  const hookFiles = [...new Set([...existing, ...plannedHookFiles])].sort();
266
266
  if (hookFiles.length === 0) {
@@ -385,7 +385,7 @@ async function buildProposalBundle(options) {
385
385
  if (templateKey === 'hooks') {
386
386
  const plannedHookFiles = templateFiles
387
387
  .map(file => path.basename(file.path))
388
- .filter(file => file.endsWith('.sh'));
388
+ .filter(file => file.endsWith('.sh') || file.endsWith('.js'));
389
389
  const settingsFile = buildHookSettings(ctx, plannedHookFiles, options);
390
390
  if (settingsFile) {
391
391
  templateFiles.push(settingsFile);
@@ -476,7 +476,7 @@ function applyRuntimeSettingsOverlays(bundle, options) {
476
476
 
477
477
  const ctx = new ProjectContext(options.dir);
478
478
  const existingHooks = ctx.hasDir('.claude/hooks')
479
- ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh'))
479
+ ? ctx.dirFiles('.claude/hooks').filter(file => file.endsWith('.sh') || file.endsWith('.js'))
480
480
  : [];
481
481
 
482
482
  const proposals = bundle.proposals.map((proposal) => {
package/src/setup.js CHANGED
@@ -782,66 +782,64 @@ ${verificationSteps.join('\n')}
782
782
  },
783
783
 
784
784
  'hooks': () => ({
785
- 'on-edit-lint.sh': `#!/bin/bash
786
- # PostToolUse hook - runs linter after file edits
787
- # Detects which linter is available and runs it
788
-
789
- if command -v npx &>/dev/null; then
790
- if [ -f "package.json" ] && grep -q '"lint"' package.json 2>/dev/null; then
791
- npm run lint --silent 2>/dev/null
792
- elif [ -f ".eslintrc" ] || [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ] || [ -f "eslint.config.js" ]; then
793
- npx eslint --fix . --quiet 2>/dev/null
794
- fi
795
- elif command -v ruff &>/dev/null; then
796
- ruff check --fix . 2>/dev/null
797
- fi
785
+ 'on-edit-lint.js': `#!/usr/bin/env node
786
+ // PostToolUse hook - runs linter after file edits
787
+ const { execSync } = require('child_process');
788
+ const fs = require('fs');
789
+ try {
790
+ if (fs.existsSync('package.json')) {
791
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
792
+ if (pkg.scripts && pkg.scripts.lint) {
793
+ execSync('npm run lint --silent', { stdio: 'ignore', timeout: 30000 });
794
+ }
795
+ }
796
+ } catch (e) { /* linter not available or failed - non-blocking */ }
798
797
  `,
799
- 'protect-secrets.sh': `#!/bin/bash
800
- # PreToolUse hook - blocks reads of secret files
801
- INPUT=$(cat -)
802
- FILE_PATH=$(echo "$INPUT" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
803
-
804
- if echo "$FILE_PATH" | grep -qiE '\\.env$|\\.env\\.|secrets/|credentials|\\.pem$|\\.key$'; then
805
- echo '{"decision": "block", "reason": "Blocked: accessing secret/credential files is not allowed."}'
806
- exit 0
807
- fi
808
- echo '{"decision": "allow"}'
798
+ 'protect-secrets.js': `#!/usr/bin/env node
799
+ // PreToolUse hook - blocks reads of secret files
800
+ let input = '';
801
+ process.stdin.on('data', d => input += d);
802
+ process.stdin.on('end', () => {
803
+ try {
804
+ const data = JSON.parse(input);
805
+ const fp = (data.tool_input && data.tool_input.file_path) || '';
806
+ if (/\\.env$|\\.env\\.|secrets[\\/\\\\]|credentials|\\.pem$|\\.key$/i.test(fp)) {
807
+ console.log(JSON.stringify({ decision: 'block', reason: 'Blocked: accessing secret/credential files is not allowed.' }));
808
+ } else {
809
+ console.log(JSON.stringify({ decision: 'allow' }));
810
+ }
811
+ } catch (e) {
812
+ console.log(JSON.stringify({ decision: 'allow' }));
813
+ }
814
+ });
809
815
  `,
810
- 'log-changes.sh': `#!/bin/bash
811
- # PostToolUse hook - logs all file changes with timestamps
812
- # Appends to .claude/logs/file-changes.log
813
-
814
- INPUT=$(cat -)
815
- TOOL_NAME=$(echo "$INPUT" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
816
- TOOL_NAME=\${TOOL_NAME:-unknown}
817
- FILE_PATH=$(echo "$INPUT" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')
818
-
819
- if [ -z "$FILE_PATH" ]; then
820
- exit 0
821
- fi
822
-
823
- LOG_DIR=".claude/logs"
824
- LOG_FILE="$LOG_DIR/file-changes.log"
825
-
826
- mkdir -p "$LOG_DIR"
827
-
828
- TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
829
- echo "[$TIMESTAMP] $TOOL_NAME: $FILE_PATH" >> "$LOG_FILE"
830
-
831
- exit 0
816
+ 'log-changes.js': `#!/usr/bin/env node
817
+ // PostToolUse hook - logs all file changes with timestamps
818
+ const fs = require('fs');
819
+ const path = require('path');
820
+ let input = '';
821
+ process.stdin.on('data', d => input += d);
822
+ process.stdin.on('end', () => {
823
+ try {
824
+ const data = JSON.parse(input);
825
+ const fp = (data.tool_input && data.tool_input.file_path) || '';
826
+ if (!fp) process.exit(0);
827
+ const toolName = data.tool_name || 'unknown';
828
+ const logDir = path.join('.claude', 'logs');
829
+ fs.mkdirSync(logDir, { recursive: true });
830
+ const ts = new Date().toISOString().replace('T', ' ').split('.')[0];
831
+ fs.appendFileSync(path.join(logDir, 'file-changes.log'), \`[\${ts}] \${toolName}: \${fp}\\n\`);
832
+ } catch (e) { /* non-blocking */ }
833
+ });
832
834
  `,
833
- 'session-start.sh': `#!/bin/bash
834
- # SessionStart hook - prepares logs and records session entry
835
-
836
- LOG_DIR=".claude/logs"
837
- LOG_FILE="$LOG_DIR/sessions.log"
838
-
839
- mkdir -p "$LOG_DIR"
840
-
841
- TIMESTAMP=$(date +"%Y-%m-%d %H:%M:%S")
842
- echo "[$TIMESTAMP] session started" >> "$LOG_FILE"
843
-
844
- exit 0
835
+ 'session-start.js': `#!/usr/bin/env node
836
+ // SessionStart hook - prepares logs and records session entry
837
+ const fs = require('fs');
838
+ const path = require('path');
839
+ const logDir = path.join('.claude', 'logs');
840
+ fs.mkdirSync(logDir, { recursive: true });
841
+ const ts = new Date().toISOString().replace('T', ' ').split('.')[0];
842
+ fs.appendFileSync(path.join(logDir, 'sessions.log'), \`[\${ts}] session started\\n\`);
845
843
  `,
846
844
  }),
847
845
 
@@ -1219,7 +1217,7 @@ async function setup(options) {
1219
1217
  const hooksDir = path.join(options.dir, '.claude/hooks');
1220
1218
  const settingsPath = path.join(options.dir, '.claude/settings.json');
1221
1219
  if (fs.existsSync(hooksDir) && !fs.existsSync(settingsPath)) {
1222
- const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh'));
1220
+ const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.sh') || f.endsWith('.js'));
1223
1221
  if (hookFiles.length > 0) {
1224
1222
  const settings = buildSettingsForProfile({
1225
1223
  profileKey: options.profile || 'safe-write',