@telelabsai/ship 1.1.3 → 1.1.6

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,143 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Git pre-commit hook — scans staged file CONTENTS for secrets.
5
+ * Reads patterns from .claude/shared/secret-patterns.json (single source of truth).
6
+ *
7
+ * Exit 0 = allow commit
8
+ * Exit 1 = block commit (secrets found)
9
+ */
10
+
11
+ const { execSync } = require('child_process');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ // Load patterns from shared config
16
+ const patternsPath = path.join(__dirname, '..', '.claude', 'shared', 'secret-patterns.json');
17
+ const patterns = JSON.parse(fs.readFileSync(patternsPath, 'utf8'));
18
+
19
+ // Get staged files
20
+ const stagedFiles = execSync('git diff --cached --name-only --diff-filter=ACM', { encoding: 'utf8' })
21
+ .trim()
22
+ .split('\n')
23
+ .filter(Boolean);
24
+
25
+ if (stagedFiles.length === 0) {
26
+ process.exit(0);
27
+ }
28
+
29
+ // Build regex patterns from all categories
30
+ const regexPatterns = [];
31
+ for (const category of ['apiKeys', 'credentials', 'databaseUrls', 'privateKeys']) {
32
+ for (const entry of patterns[category]) {
33
+ regexPatterns.push({
34
+ name: entry.name,
35
+ regex: new RegExp(entry.pattern, 'g'),
36
+ severity: entry.severity,
37
+ });
38
+ }
39
+ }
40
+
41
+ // Build false positive checker
42
+ const falsePositives = patterns.falsePositives;
43
+ function isFalsePositive(line) {
44
+ const trimmed = line.trim();
45
+ if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*')) {
46
+ return false; // Don't skip comments — secrets in comments are still secrets
47
+ }
48
+ return falsePositives.some((fp) => line.includes(fp));
49
+ }
50
+
51
+ // Skip binary and irrelevant files
52
+ const skipExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.mp4', '.mp3', '.zip', '.tar', '.gz'];
53
+ const skipFiles = ['package-lock.json', 'yarn.lock', 'pnpm-lock.yaml'];
54
+
55
+ // Check dangerous files being committed
56
+ const findings = [];
57
+
58
+ for (const file of stagedFiles) {
59
+ // Check if file itself is dangerous
60
+ for (const dangerousFile of patterns.dangerousFiles) {
61
+ const pattern = dangerousFile.replace(/\*/g, '.*');
62
+ if (new RegExp(`(^|/)${pattern}$`).test(file)) {
63
+ findings.push({
64
+ file,
65
+ line: 0,
66
+ name: `Sensitive file: ${dangerousFile}`,
67
+ severity: 'critical',
68
+ match: file,
69
+ });
70
+ }
71
+ }
72
+
73
+ // Skip binary files
74
+ const ext = path.extname(file).toLowerCase();
75
+ if (skipExtensions.includes(ext)) continue;
76
+ if (skipFiles.includes(path.basename(file))) continue;
77
+
78
+ // Read staged content
79
+ let content;
80
+ try {
81
+ content = execSync(`git show :${file}`, { encoding: 'utf8' });
82
+ } catch {
83
+ continue;
84
+ }
85
+
86
+ // Scan each line
87
+ const lines = content.split('\n');
88
+ for (let i = 0; i < lines.length; i++) {
89
+ const line = lines[i];
90
+
91
+ if (isFalsePositive(line)) continue;
92
+
93
+ for (const { name, regex, severity } of regexPatterns) {
94
+ regex.lastIndex = 0;
95
+ if (regex.test(line)) {
96
+ findings.push({
97
+ file,
98
+ line: i + 1,
99
+ name,
100
+ severity,
101
+ match: line.trim().substring(0, 80),
102
+ });
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ if (findings.length === 0) {
109
+ process.exit(0);
110
+ }
111
+
112
+ // Report findings
113
+ console.error('\n Ship: Secrets detected in staged files!\n');
114
+
115
+ const critical = findings.filter((f) => f.severity === 'critical');
116
+ const high = findings.filter((f) => f.severity === 'high');
117
+ const medium = findings.filter((f) => f.severity === 'medium');
118
+
119
+ if (critical.length) {
120
+ console.error(' Critical:');
121
+ for (const f of critical) {
122
+ console.error(` ${f.file}:${f.line} — ${f.name}`);
123
+ if (f.match && f.line > 0) console.error(` ${f.match}`);
124
+ }
125
+ }
126
+
127
+ if (high.length) {
128
+ console.error('\n High:');
129
+ for (const f of high) {
130
+ console.error(` ${f.file}:${f.line} — ${f.name}`);
131
+ }
132
+ }
133
+
134
+ if (medium.length) {
135
+ console.error('\n Medium:');
136
+ for (const f of medium) {
137
+ console.error(` ${f.file}:${f.line} — ${f.name}`);
138
+ }
139
+ }
140
+
141
+ console.error(`\n ${findings.length} secret(s) found. Commit blocked.`);
142
+ console.error(' Fix: Move secrets to environment variables or add files to .gitignore.\n');
143
+ process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telelabsai/ship",
3
- "version": "1.1.3",
3
+ "version": "1.1.6",
4
4
  "description": "Ship code faster with pre-configured agents, skills, rules, and hooks for Claude Code.",
5
5
  "bin": {
6
6
  "ship": "cli/bin.js"
@@ -8,6 +8,7 @@
8
8
  "files": [
9
9
  "cli/",
10
10
  ".claude/",
11
+ "git-hooks/",
11
12
  "CLAUDE.md",
12
13
  "README.md",
13
14
  "LICENSE"