claude-code-starter 0.14.1 → 0.15.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.
Files changed (2) hide show
  1. package/dist/cli.js +414 -14
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { execSync, spawn } from "child_process";
5
- import fs3 from "fs";
6
- import path3 from "path";
5
+ import fs5 from "fs";
6
+ import path5 from "path";
7
7
  import { fileURLToPath } from "url";
8
8
  import ora from "ora";
9
9
  import pc from "picocolors";
@@ -599,6 +599,239 @@ function writeSettings(rootDir, stack) {
599
599
  fs2.writeFileSync(fullPath, content);
600
600
  }
601
601
 
602
+ // src/hooks.ts
603
+ import fs3 from "fs";
604
+ import path3 from "path";
605
+ var HOOK_SCRIPT = String.raw`#!/usr/bin/env node
606
+ /**
607
+ * Block Dangerous Commands - PreToolUse Hook for Bash
608
+ * Blocks dangerous patterns before execution.
609
+ *
610
+ * SAFETY_LEVEL: 'critical' | 'high' | 'strict'
611
+ * critical - Only catastrophic: rm -rf ~, dd to disk, fork bombs
612
+ * high - + risky: force push main, secrets exposure, git reset --hard
613
+ * strict - + cautionary: any force push, sudo rm, docker prune
614
+ */
615
+
616
+ const fs = require('fs');
617
+ const path = require('path');
618
+
619
+ const SAFETY_LEVEL = 'high';
620
+
621
+ const PATTERNS = [
622
+ // CRITICAL — Catastrophic, unrecoverable
623
+
624
+ // Filesystem destruction
625
+ { level: 'critical', id: 'rm-home', regex: /\brm\s+(-.+\s+)*["']?~\/?["']?(\s|$|[;&|])/, reason: 'rm targeting home directory' },
626
+ { level: 'critical', id: 'rm-home-var', regex: /\brm\s+(-.+\s+)*["']?\$HOME["']?(\s|$|[;&|])/, reason: 'rm targeting $HOME' },
627
+ { level: 'critical', id: 'rm-home-trailing', regex: /\brm\s+.+\s+["']?(~\/?|\$HOME)["']?(\s*$|[;&|])/, reason: 'rm with trailing ~/ or $HOME' },
628
+ { level: 'critical', id: 'rm-root', regex: /\brm\s+(-.+\s+)*\/(\*|\s|$|[;&|])/, reason: 'rm targeting root filesystem' },
629
+ { level: 'critical', id: 'rm-system', regex: /\brm\s+(-.+\s+)*\/(etc|usr|var|bin|sbin|lib|boot|dev|proc|sys)(\/|\s|$)/, reason: 'rm targeting system directory' },
630
+ { level: 'critical', id: 'rm-cwd', regex: /\brm\s+(-.+\s+)*(\.\/?|\*|\.\/\*)(\s|$|[;&|])/, reason: 'rm deleting current directory contents' },
631
+
632
+ // Disk operations
633
+ { level: 'critical', id: 'dd-disk', regex: /\bdd\b.+of=\/dev\/(sd[a-z]|nvme|hd[a-z]|vd[a-z]|xvd[a-z])/, reason: 'dd writing to disk device' },
634
+ { level: 'critical', id: 'mkfs', regex: /\bmkfs(\.\w+)?\s+\/dev\/(sd[a-z]|nvme|hd[a-z]|vd[a-z])/, reason: 'mkfs formatting disk' },
635
+ { level: 'critical', id: 'fdisk', regex: /\b(fdisk|wipefs|parted)\s+\/dev\//, reason: 'disk partitioning/wiping operation' },
636
+
637
+ // Shell exploits
638
+ { level: 'critical', id: 'fork-bomb', regex: /:\(\)\s*\{.*:\s*\|\s*:.*&/, reason: 'fork bomb detected' },
639
+
640
+ // Git — history destruction
641
+ { level: 'critical', id: 'git-filter', regex: /\bgit\s+(filter-branch|filter-repo)\b/, reason: 'git history rewriting blocked' },
642
+ { level: 'critical', id: 'git-reflog-exp', regex: /\bgit\s+(reflog\s+expire|gc\s+--prune|prune)\b/, reason: 'removes git recovery safety net' },
643
+
644
+ // HIGH — Significant risk, data loss, security exposure
645
+
646
+ // Remote code execution
647
+ { level: 'high', id: 'curl-pipe-sh', regex: /\b(curl|wget)\b.+\|\s*(ba)?sh\b/, reason: 'piping URL to shell (RCE risk)' },
648
+
649
+ // Git — destructive operations
650
+ { level: 'high', id: 'git-force-main', regex: /\bgit\s+push\b(?!.+--force-with-lease).+(--force|-f)\b.+\b(main|master)\b/, reason: 'force push to main/master' },
651
+ { level: 'high', id: 'git-reset-hard', regex: /\bgit\s+reset\s+--hard/, reason: 'git reset --hard loses uncommitted work' },
652
+ { level: 'high', id: 'git-clean-f', regex: /\bgit\s+clean\s+(-\w*f|-f)/, reason: 'git clean -f deletes untracked files' },
653
+ { level: 'high', id: 'git-no-verify', regex: /\bgit\b.+--no-verify/, reason: '--no-verify skips safety hooks' },
654
+ { level: 'high', id: 'git-stash-destruct', regex: /\bgit\s+stash\s+(drop|clear|pop)\b/, reason: 'destructive git stash operation' },
655
+ { level: 'high', id: 'git-branch-D', regex: /\bgit\s+branch\s+(-D|--delete\s+--force)\b/, reason: 'git branch -D force-deletes branch' },
656
+ { level: 'high', id: 'git-checkout-force', regex: /\bgit\s+checkout\s+(-f|--\s+\.)/, reason: 'git checkout -f/-- . discards changes' },
657
+ { level: 'high', id: 'git-restore-destruct', regex: /\bgit\s+restore\s+(--staged\s+--worktree|\.)/, reason: 'git restore discards changes' },
658
+ { level: 'high', id: 'git-update-ref', regex: /\bgit\s+(update-ref|symbolic-ref|replace)\b/, reason: 'git ref manipulation blocked' },
659
+ { level: 'high', id: 'git-config-global', regex: /\bgit\s+config\s+--(global|system)\b/, reason: 'git global/system config blocked' },
660
+ { level: 'high', id: 'git-tag-delete', regex: /\bgit\s+tag\s+(-d|--delete)\b/, reason: 'git tag deletion blocked' },
661
+
662
+ // Git — write operations (user handles manually)
663
+ { level: 'high', id: 'git-push', regex: /\bgit\s+push\b/, reason: 'git push blocked — user handles manually' },
664
+ { level: 'high', id: 'git-pull', regex: /\bgit\s+pull\b/, reason: 'git pull blocked — user handles manually' },
665
+ { level: 'high', id: 'git-fetch', regex: /\bgit\s+fetch\b/, reason: 'git fetch blocked — user handles manually' },
666
+ { level: 'high', id: 'git-clone', regex: /\bgit\s+clone\b/, reason: 'git clone blocked — user handles manually' },
667
+ { level: 'high', id: 'git-add', regex: /\bgit\s+(add|stage)\b/, reason: 'git add/stage blocked — user handles manually' },
668
+ { level: 'high', id: 'git-commit', regex: /\bgit\s+commit\b/, reason: 'git commit blocked — user handles manually' },
669
+ { level: 'high', id: 'git-merge', regex: /\bgit\s+merge\b/, reason: 'git merge blocked — user handles manually' },
670
+ { level: 'high', id: 'git-rebase', regex: /\bgit\s+rebase\b/, reason: 'git rebase blocked — user handles manually' },
671
+ { level: 'high', id: 'git-reset', regex: /\bgit\s+reset\b/, reason: 'git reset blocked — user handles manually' },
672
+ { level: 'high', id: 'git-remote-mod', regex: /\bgit\s+remote\s+(add|set-url|remove)\b/, reason: 'git remote modification blocked' },
673
+ { level: 'high', id: 'git-submodule', regex: /\bgit\s+submodule\s+(add|update)\b/, reason: 'git submodule operation blocked' },
674
+
675
+ // Credentials & secrets
676
+ { level: 'high', id: 'chmod-777', regex: /\bchmod\b.+\b777\b/, reason: 'chmod 777 is a security risk' },
677
+ { level: 'high', id: 'cat-env', regex: /\b(cat|less|head|tail|more)\s+\.env\b/, reason: 'reading .env file exposes secrets' },
678
+ { level: 'high', id: 'cat-secrets', regex: /\b(cat|less|head|tail|more)\b.+(credentials|secrets?|\.pem|\.key|id_rsa|id_ed25519)/i, reason: 'reading secrets file' },
679
+ { level: 'high', id: 'env-dump', regex: /\b(printenv|^env)\s*([;&|]|$)/, reason: 'env dump may expose secrets' },
680
+ { level: 'high', id: 'echo-secret', regex: /\becho\b.+\$\w*(SECRET|KEY|TOKEN|PASSWORD|API_|PRIVATE)/i, reason: 'echoing secret variable' },
681
+ { level: 'high', id: 'rm-ssh', regex: /\brm\b.+\.ssh\/(id_|authorized_keys|known_hosts)/, reason: 'deleting SSH keys' },
682
+ { level: 'high', id: 'security-keychain', regex: /\bsecurity\s+find-generic-password\b/, reason: 'keychain access blocked' },
683
+ { level: 'high', id: 'gpg-export-secret', regex: /\bgpg\s+--export-secret-keys\b/, reason: 'GPG secret key export blocked' },
684
+ { level: 'high', id: 'history-cmd', regex: /\bhistory\b/, reason: 'history may expose secrets' },
685
+
686
+ // Destructive system commands
687
+ { level: 'high', id: 'elevated-priv', regex: /\b(sudo|doas|pkexec)\b/, reason: 'elevated privilege command blocked' },
688
+ { level: 'high', id: 'su-cmd', regex: /\bsu\b/, reason: 'su (switch user) blocked' },
689
+ { level: 'high', id: 'chmod-R', regex: /\bchmod\s+(-\w*R|-R)/, reason: 'recursive chmod blocked' },
690
+ { level: 'high', id: 'chown-R', regex: /\bchown\s+(-\w*R|-R)/, reason: 'recursive chown blocked' },
691
+ { level: 'high', id: 'kill-all', regex: /\bkill\s+-9\s+-1\b/, reason: 'kill all processes blocked' },
692
+ { level: 'high', id: 'killall', regex: /\b(killall|pkill\s+-9)\b/, reason: 'mass process killing blocked' },
693
+ { level: 'high', id: 'truncate-zero', regex: /\btruncate\s+-s\s*0\b/, reason: 'truncating file to zero blocked' },
694
+ { level: 'high', id: 'empty-file', regex: /\bcat\s+\/dev\/null\s*>/, reason: 'emptying file via /dev/null blocked' },
695
+ { level: 'high', id: 'crontab-r', regex: /\bcrontab\s+-r/, reason: 'removes all cron jobs' },
696
+
697
+ // Docker
698
+ { level: 'high', id: 'docker-vol-rm', regex: /\bdocker\s+volume\s+(rm|prune)/, reason: 'docker volume deletion loses data' },
699
+ { level: 'high', id: 'docker-push', regex: /\bdocker\s+push\b/, reason: 'docker push blocked' },
700
+ { level: 'high', id: 'docker-rm-all', regex: /\bdocker\s+rm\s+-f\b.+\$\(docker\s+ps/, reason: 'docker rm all containers blocked' },
701
+ { level: 'high', id: 'docker-sys-prune-a', regex: /\bdocker\s+system\s+prune\s+-a/, reason: 'docker system prune -a blocked' },
702
+ { level: 'high', id: 'docker-compose-destr', regex: /\bdocker[\s-]compose\s+down\s+(-v|--rmi)/, reason: 'docker-compose destructive down blocked' },
703
+
704
+ // Publishing & deployment
705
+ { level: 'high', id: 'npm-publish', regex: /\bnpm\s+(publish|unpublish|deprecate)\b/, reason: 'npm publishing blocked' },
706
+ { level: 'high', id: 'npm-audit-force', regex: /\bnpm\s+audit\s+fix\s+--force\b/, reason: 'npm audit fix --force can break deps' },
707
+ { level: 'high', id: 'cargo-publish', regex: /\bcargo\s+publish\b/, reason: 'cargo publish blocked' },
708
+ { level: 'high', id: 'pip-twine-upload', regex: /\b(pip|twine)\s+upload\b/, reason: 'Python package upload blocked' },
709
+ { level: 'high', id: 'gem-push', regex: /\bgem\s+push\b/, reason: 'gem push blocked' },
710
+ { level: 'high', id: 'pod-push', regex: /\bpod\s+trunk\s+push\b/, reason: 'pod trunk push blocked' },
711
+ { level: 'high', id: 'vercel-prod', regex: /\bvercel\b.+--prod/, reason: 'vercel production deploy blocked' },
712
+ { level: 'high', id: 'netlify-prod', regex: /\bnetlify\s+deploy\b.+--prod/, reason: 'netlify production deploy blocked' },
713
+ { level: 'high', id: 'fly-deploy', regex: /\bfly\s+deploy\b/, reason: 'fly deploy blocked' },
714
+ { level: 'high', id: 'firebase-deploy', regex: /\bfirebase\s+deploy\b/, reason: 'firebase deploy blocked' },
715
+ { level: 'high', id: 'terraform', regex: /\bterraform\s+(apply|destroy)\b/, reason: 'terraform apply/destroy blocked' },
716
+ { level: 'high', id: 'pulumi-cdktf', regex: /\b(pulumi|cdktf)\s+destroy\b/, reason: 'infrastructure destroy blocked' },
717
+ { level: 'high', id: 'kubectl-mutate', regex: /\bkubectl\s+(apply|delete|drain)\b/, reason: 'kubectl mutating operation blocked' },
718
+ { level: 'high', id: 'kubectl-scale-zero', regex: /\bkubectl\s+scale\b.+--replicas=0/, reason: 'kubectl scale to zero blocked' },
719
+ { level: 'high', id: 'helm-ops', regex: /\bhelm\s+(install|uninstall|upgrade)\b/, reason: 'helm operation blocked' },
720
+ { level: 'high', id: 'heroku', regex: /\bheroku\b/, reason: 'heroku command blocked' },
721
+ { level: 'high', id: 'eb-terminate', regex: /\beb\s+terminate\b/, reason: 'eb terminate blocked' },
722
+ { level: 'high', id: 'serverless-remove', regex: /\bserverless\s+remove\b/, reason: 'serverless remove blocked' },
723
+ { level: 'high', id: 'cap-prod-deploy', regex: /\bcap\s+production\s+deploy\b/, reason: 'production deploy blocked' },
724
+ { level: 'high', id: 'cloud-delete', regex: /\b(aws\s+cloudformation\s+delete-stack|gcloud\s+projects\s+delete|az\s+group\s+delete)\b/, reason: 'cloud resource deletion blocked' },
725
+
726
+ // Network & infrastructure
727
+ { level: 'high', id: 'curl-mutating', regex: /\bcurl\b.+-X\s*(POST|PUT|DELETE|PATCH)\b/, reason: 'mutating HTTP request blocked' },
728
+ { level: 'high', id: 'ssh-remote', regex: /\bssh\s/, reason: 'SSH remote connection blocked' },
729
+ { level: 'high', id: 'scp-remote', regex: /\bscp\s/, reason: 'SCP remote copy blocked' },
730
+ { level: 'high', id: 'rsync-delete', regex: /\brsync\b.+--delete/, reason: 'rsync --delete blocked' },
731
+ { level: 'high', id: 'firewall', regex: /\b(iptables\s+-F|ufw\s+disable)\b/, reason: 'firewall manipulation blocked' },
732
+ { level: 'high', id: 'network-kill', regex: /\bifconfig\s+\w+\s+down\b/, reason: 'network interface down blocked' },
733
+ { level: 'high', id: 'route-delete', regex: /\broute\s+del\s+default\b/, reason: 'default route deletion blocked' },
734
+
735
+ // Database
736
+ { level: 'high', id: 'sql-drop', regex: /\b(DROP\s+(DATABASE|TABLE)|TRUNCATE\s+TABLE)\b/i, reason: 'SQL drop/truncate blocked' },
737
+ { level: 'high', id: 'sql-mass-delete', regex: /\bDELETE\s+FROM\b.+\bWHERE\s+1\s*=\s*1/i, reason: 'SQL mass delete blocked' },
738
+ { level: 'high', id: 'redis-flush', regex: /\bredis-cli\s+(FLUSHALL|FLUSHDB)\b/, reason: 'redis flush blocked' },
739
+ { level: 'high', id: 'orm-reset', regex: /\b(prisma\s+migrate\s+reset|rails\s+db:(drop|reset)|django\s+flush)\b/, reason: 'ORM database reset blocked' },
740
+ { level: 'high', id: 'alembic-downgrade', regex: /\balembic\s+downgrade\s+base\b/, reason: 'alembic downgrade base blocked' },
741
+ { level: 'high', id: 'mongo-drop', regex: /\bmongosh\b.+dropDatabase/, reason: 'MongoDB drop database blocked' },
742
+
743
+ // STRICT — Cautionary, context-dependent
744
+ { level: 'strict', id: 'git-checkout-dot', regex: /\bgit\s+checkout\s+\./, reason: 'git checkout . discards changes' },
745
+ { level: 'strict', id: 'docker-prune', regex: /\bdocker\s+(system|image)\s+prune/, reason: 'docker prune removes images' },
746
+ ];
747
+
748
+ const LEVELS = { critical: 1, high: 2, strict: 3 };
749
+ const EMOJIS = { critical: '\u{1F6A8}', high: '\u26D4', strict: '\u26A0\uFE0F' };
750
+ const LOG_DIR = path.join(process.env.HOME || '/tmp', '.claude', 'hooks-logs');
751
+
752
+ function log(data) {
753
+ try {
754
+ if (!fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
755
+ const file = path.join(LOG_DIR, new Date().toISOString().slice(0, 10) + '.jsonl');
756
+ fs.appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), ...data }) + '\n');
757
+ } catch {}
758
+ }
759
+
760
+ function checkCommand(cmd, safetyLevel) {
761
+ safetyLevel = safetyLevel || SAFETY_LEVEL;
762
+ const threshold = LEVELS[safetyLevel] || 2;
763
+ for (const p of PATTERNS) {
764
+ if (LEVELS[p.level] <= threshold && p.regex.test(cmd)) {
765
+ return { blocked: true, pattern: p };
766
+ }
767
+ }
768
+ return { blocked: false, pattern: null };
769
+ }
770
+
771
+ async function main() {
772
+ let input = '';
773
+ for await (const chunk of process.stdin) input += chunk;
774
+
775
+ try {
776
+ const data = JSON.parse(input);
777
+ const { tool_name, tool_input, session_id, cwd, permission_mode } = data;
778
+ if (tool_name !== 'Bash') return console.log('{}');
779
+
780
+ const cmd = tool_input?.command || '';
781
+ const result = checkCommand(cmd);
782
+
783
+ if (result.blocked) {
784
+ const p = result.pattern;
785
+ log({ level: 'BLOCKED', id: p.id, priority: p.level, cmd, session_id, cwd, permission_mode });
786
+ return console.log(JSON.stringify({
787
+ hookSpecificOutput: {
788
+ hookEventName: 'PreToolUse',
789
+ permissionDecision: 'deny',
790
+ permissionDecisionReason: EMOJIS[p.level] + ' [' + p.id + '] ' + p.reason
791
+ }
792
+ }));
793
+ }
794
+ console.log('{}');
795
+ } catch (e) {
796
+ log({ level: 'ERROR', error: e.message });
797
+ console.log('{}');
798
+ }
799
+ }
800
+
801
+ if (require.main === module) {
802
+ main();
803
+ } else {
804
+ module.exports = { PATTERNS, LEVELS, SAFETY_LEVEL, checkCommand };
805
+ }
806
+ `;
807
+ function installHook(rootDir) {
808
+ const hooksDir = path3.join(rootDir, ".claude", "hooks");
809
+ const hookPath = path3.join(hooksDir, "block-dangerous-commands.js");
810
+ const settingsPath = path3.join(rootDir, ".claude", "settings.json");
811
+ fs3.mkdirSync(hooksDir, { recursive: true });
812
+ fs3.writeFileSync(hookPath, HOOK_SCRIPT);
813
+ fs3.chmodSync(hookPath, 493);
814
+ try {
815
+ const existing = fs3.existsSync(settingsPath) ? JSON.parse(fs3.readFileSync(settingsPath, "utf-8")) : {};
816
+ existing.hooks = {
817
+ ...existing.hooks,
818
+ PreToolUse: [
819
+ {
820
+ matcher: "Bash",
821
+ hooks: [
822
+ {
823
+ type: "command",
824
+ command: "node .claude/hooks/block-dangerous-commands.js"
825
+ }
826
+ ]
827
+ }
828
+ ]
829
+ };
830
+ fs3.writeFileSync(settingsPath, JSON.stringify(existing, null, 2));
831
+ } catch {
832
+ }
833
+ }
834
+
602
835
  // src/prompt.ts
603
836
  function getAnalysisPrompt(projectInfo) {
604
837
  const context = buildContextSection(projectInfo);
@@ -1272,10 +1505,154 @@ Body: This command delegates to the code-reviewer agent for thorough review.
1272
1505
  3. If the agent is unavailable, perform a lightweight review: run the linter and check for obvious issues
1273
1506
  Do NOT duplicate the code-reviewer agent's checklist here \u2014 the agent has the full review criteria.`;
1274
1507
 
1508
+ // src/validator.ts
1509
+ import fs4 from "fs";
1510
+ import path4 from "path";
1511
+ function extractCommands(claudeMd) {
1512
+ const commands = [];
1513
+ const match = claudeMd.match(/## Common Commands[\s\S]*?```(?:bash)?\n([\s\S]*?)```/);
1514
+ if (!match) return commands;
1515
+ for (const line of match[1].split("\n")) {
1516
+ const trimmed = line.trim();
1517
+ if (!trimmed || trimmed.startsWith("#")) continue;
1518
+ const cmd = trimmed.split(/\s+#/)[0].trim();
1519
+ if (cmd.length > 3) commands.push(cmd);
1520
+ }
1521
+ return commands;
1522
+ }
1523
+ function extractConventionFingerprints(claudeMd) {
1524
+ const fingerprints = [];
1525
+ const startIdx = claudeMd.indexOf("## Code Conventions");
1526
+ if (startIdx === -1) return fingerprints;
1527
+ const rest = claudeMd.slice(startIdx + "## Code Conventions".length);
1528
+ const nextHeading = rest.match(/\n## [A-Z]/);
1529
+ const section = nextHeading ? claudeMd.slice(startIdx, startIdx + "## Code Conventions".length + nextHeading.index) : claudeMd.slice(startIdx);
1530
+ for (const kw of ["camelCase", "PascalCase", "kebab-case", "snake_case"]) {
1531
+ if (section.includes(kw)) fingerprints.push(kw);
1532
+ }
1533
+ if (/\bnamed exports?\b/i.test(section)) fingerprints.push("named export");
1534
+ if (/\bdefault exports?\b/i.test(section)) fingerprints.push("default export");
1535
+ if (section.includes("import type")) fingerprints.push("import type");
1536
+ for (const kw of [".skip()", ".only()", "console.log"]) {
1537
+ if (section.includes(kw)) fingerprints.push(kw);
1538
+ }
1539
+ return fingerprints;
1540
+ }
1541
+ var RULE_WORDS = /\b(verify|check|ensure|always|never|must|should|avoid)\b/i;
1542
+ function isConventionDuplication(line, fingerprints) {
1543
+ const trimmed = line.trim();
1544
+ if (!trimmed || trimmed.startsWith("#") || trimmed.includes("CLAUDE.md")) return false;
1545
+ if (!/^[-*]\s/.test(trimmed)) return false;
1546
+ const matchCount = fingerprints.filter((fp) => trimmed.includes(fp)).length;
1547
+ if (matchCount >= 2) return true;
1548
+ if (matchCount === 1 && RULE_WORDS.test(trimmed)) return true;
1549
+ return false;
1550
+ }
1551
+ function findLiteralCommand(line, commands) {
1552
+ const trimmed = line.trim();
1553
+ if (!trimmed || trimmed.startsWith("#") || trimmed.includes("CLAUDE.md")) return null;
1554
+ for (const cmd of commands) {
1555
+ if (trimmed.includes(cmd)) return cmd;
1556
+ }
1557
+ return null;
1558
+ }
1559
+ function separateFrontmatter(content) {
1560
+ const match = content.match(/^---\n[\s\S]*?\n---(?:\n|$)/);
1561
+ if (!match) {
1562
+ return { frontmatter: "", body: content };
1563
+ }
1564
+ return {
1565
+ frontmatter: match[0],
1566
+ body: content.slice(match[0].length)
1567
+ };
1568
+ }
1569
+ function processFile(filePath, commands, fingerprints) {
1570
+ const content = fs4.readFileSync(filePath, "utf-8");
1571
+ const { frontmatter, body } = separateFrontmatter(content);
1572
+ const lines = body.split("\n");
1573
+ const changes = [];
1574
+ const newLines = [];
1575
+ let inCodeBlock = false;
1576
+ for (const line of lines) {
1577
+ if (line.trim().startsWith("```")) {
1578
+ inCodeBlock = !inCodeBlock;
1579
+ newLines.push(line);
1580
+ continue;
1581
+ }
1582
+ if (inCodeBlock) {
1583
+ newLines.push(line);
1584
+ continue;
1585
+ }
1586
+ if (isConventionDuplication(line, fingerprints)) {
1587
+ changes.push({ file: filePath, original: line.trim(), replacement: null });
1588
+ continue;
1589
+ }
1590
+ const cmd = findLiteralCommand(line, commands);
1591
+ if (cmd) {
1592
+ const newLine = line.replace(cmd, "see Common Commands in CLAUDE.md");
1593
+ changes.push({ file: filePath, original: line.trim(), replacement: newLine.trim() });
1594
+ newLines.push(newLine);
1595
+ continue;
1596
+ }
1597
+ newLines.push(line);
1598
+ }
1599
+ if (changes.length > 0) {
1600
+ fs4.writeFileSync(filePath, frontmatter + newLines.join("\n"));
1601
+ }
1602
+ return changes;
1603
+ }
1604
+ function walkMdFiles(dir) {
1605
+ const files = [];
1606
+ if (!fs4.existsSync(dir)) return files;
1607
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
1608
+ for (const entry of entries) {
1609
+ const fullPath = path4.join(dir, entry.name);
1610
+ if (entry.isDirectory()) {
1611
+ files.push(...walkMdFiles(fullPath));
1612
+ } else if (entry.name.endsWith(".md")) {
1613
+ files.push(fullPath);
1614
+ }
1615
+ }
1616
+ return files;
1617
+ }
1618
+ function validateArtifacts(rootDir) {
1619
+ const result = {
1620
+ filesChecked: 0,
1621
+ filesModified: 0,
1622
+ duplicationsRemoved: 0,
1623
+ changes: []
1624
+ };
1625
+ const claudeMdPath = path4.join(rootDir, ".claude", "CLAUDE.md");
1626
+ if (!fs4.existsSync(claudeMdPath)) return result;
1627
+ try {
1628
+ const claudeMd = fs4.readFileSync(claudeMdPath, "utf-8");
1629
+ const commands = extractCommands(claudeMd);
1630
+ const fingerprints = extractConventionFingerprints(claudeMd);
1631
+ if (commands.length === 0 && fingerprints.length === 0) return result;
1632
+ const claudeDir = path4.join(rootDir, ".claude");
1633
+ const files = walkMdFiles(claudeDir).filter((f) => !f.endsWith("CLAUDE.md"));
1634
+ for (const filePath of files) {
1635
+ result.filesChecked++;
1636
+ const changes = processFile(filePath, commands, fingerprints);
1637
+ if (changes.length > 0) {
1638
+ result.filesModified++;
1639
+ result.duplicationsRemoved += changes.length;
1640
+ for (const change of changes) {
1641
+ change.file = path4.relative(rootDir, filePath);
1642
+ }
1643
+ result.changes.push(...changes);
1644
+ }
1645
+ }
1646
+ } catch {
1647
+ return result;
1648
+ }
1649
+ return result;
1650
+ }
1651
+
1275
1652
  // src/cli.ts
1276
- var __dirname2 = path3.dirname(fileURLToPath(import.meta.url));
1653
+ var __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
1277
1654
  var VERSION = JSON.parse(
1278
- fs3.readFileSync(path3.join(__dirname2, "..", "package.json"), "utf-8")
1655
+ fs5.readFileSync(path5.join(__dirname2, "..", "package.json"), "utf-8")
1279
1656
  ).version;
1280
1657
  function parseArgs(args) {
1281
1658
  return {
@@ -1313,7 +1690,7 @@ ${pc.bold("WHAT IT DOES")}
1313
1690
  - Skills for your frameworks and workflows
1314
1691
  - Agents for code review and testing
1315
1692
  - Rules matching your code style
1316
- - Commands for task management
1693
+ - Commands for analysis and code review
1317
1694
 
1318
1695
  ${pc.bold("REQUIREMENTS")}
1319
1696
  Claude CLI must be installed: https://claude.ai/download
@@ -1789,17 +2166,17 @@ function runClaudeAnalysis(projectDir, projectInfo) {
1789
2166
  });
1790
2167
  }
1791
2168
  function getGeneratedFiles(projectDir) {
1792
- const claudeDir = path3.join(projectDir, ".claude");
2169
+ const claudeDir = path5.join(projectDir, ".claude");
1793
2170
  const files = [];
1794
2171
  function walk(dir) {
1795
- if (!fs3.existsSync(dir)) return;
1796
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
2172
+ if (!fs5.existsSync(dir)) return;
2173
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
1797
2174
  for (const entry of entries) {
1798
- const fullPath = path3.join(dir, entry.name);
2175
+ const fullPath = path5.join(dir, entry.name);
1799
2176
  if (entry.isDirectory()) {
1800
2177
  walk(fullPath);
1801
2178
  } else {
1802
- files.push(path3.relative(projectDir, fullPath));
2179
+ files.push(path5.relative(projectDir, fullPath));
1803
2180
  }
1804
2181
  }
1805
2182
  }
@@ -1856,7 +2233,7 @@ async function main() {
1856
2233
  const { proceed } = await prompts({
1857
2234
  type: "confirm",
1858
2235
  name: "proceed",
1859
- message: "Update existing configuration? (preserves task state)",
2236
+ message: "Update existing configuration?",
1860
2237
  initial: true
1861
2238
  });
1862
2239
  if (!proceed) {
@@ -1883,6 +2260,14 @@ async function main() {
1883
2260
  console.error(pc.red("Claude analysis failed. Please try again."));
1884
2261
  process.exit(1);
1885
2262
  }
2263
+ const validation = validateArtifacts(projectDir);
2264
+ if (validation.duplicationsRemoved > 0) {
2265
+ console.log(
2266
+ pc.gray(
2267
+ ` Deduplication: removed ${validation.duplicationsRemoved} redundancies from ${validation.filesModified} files`
2268
+ )
2269
+ );
2270
+ }
1886
2271
  const generatedFiles = getGeneratedFiles(projectDir);
1887
2272
  console.log();
1888
2273
  console.log(pc.green(`Done! (${generatedFiles.length} files)`));
@@ -1897,12 +2282,12 @@ async function main() {
1897
2282
  }
1898
2283
  if (skills.length > 0) {
1899
2284
  console.log(
1900
- ` ${skills.length} skills (${skills.map((s) => path3.basename(s, ".md")).join(", ")})`
2285
+ ` ${skills.length} skills (${skills.map((s) => path5.basename(s, ".md")).join(", ")})`
1901
2286
  );
1902
2287
  }
1903
2288
  if (agents.length > 0) {
1904
2289
  console.log(
1905
- ` ${agents.length} agents (${agents.map((a) => path3.basename(a, ".md")).join(", ")})`
2290
+ ` ${agents.length} agents (${agents.map((a) => path5.basename(a, ".md")).join(", ")})`
1906
2291
  );
1907
2292
  }
1908
2293
  if (rules.length > 0) {
@@ -1912,6 +2297,21 @@ async function main() {
1912
2297
  console.log(` ${commands.length} commands`);
1913
2298
  }
1914
2299
  console.log();
2300
+ if (args.interactive) {
2301
+ console.log();
2302
+ const { installSafetyHook } = await prompts({
2303
+ type: "confirm",
2304
+ name: "installSafetyHook",
2305
+ message: "Add a safety hook to block dangerous commands? (git push, rm -rf, etc.)",
2306
+ initial: true
2307
+ });
2308
+ if (installSafetyHook) {
2309
+ installHook(projectDir);
2310
+ console.log(pc.green(" + .claude/hooks/block-dangerous-commands.js"));
2311
+ console.log(pc.gray(" Blocks destructive Bash commands before execution"));
2312
+ }
2313
+ }
2314
+ console.log();
1915
2315
  console.log(`${pc.cyan("Next step:")} Run ${pc.bold("claude")} to start working!`);
1916
2316
  console.log();
1917
2317
  console.log(
@@ -1921,7 +2321,7 @@ async function main() {
1921
2321
  );
1922
2322
  }
1923
2323
  try {
1924
- const isMain = process.argv[1] && fs3.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
2324
+ const isMain = process.argv[1] && fs5.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
1925
2325
  if (isMain) {
1926
2326
  main().catch((err) => {
1927
2327
  console.error(pc.red("Error:"), err.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-starter",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "description": "A lightweight starter kit for AI-assisted development with Claude Code",
5
5
  "keywords": [
6
6
  "claude",