hardstop 1.4.7 → 1.4.9

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/bin/install.js CHANGED
@@ -1,310 +1,322 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
-
7
- const PLUGIN_NAME = 'hs';
8
- const CLAUDE_DIR = path.join(os.homedir(), '.claude');
9
- const PLUGINS_DIR = path.join(CLAUDE_DIR, 'plugins');
10
- const PLUGIN_DIR = path.join(PLUGINS_DIR, PLUGIN_NAME);
11
- const SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
12
- const SKILL_DIR = path.join(SKILLS_DIR, PLUGIN_NAME);
13
- const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
14
-
15
- function detectClaude() {
16
- // Check if Claude Code is installed
17
- // Check for settings.json or config.json
18
- const settingsFile = path.join(CLAUDE_DIR, 'settings.json');
19
- const configFile = path.join(CLAUDE_DIR, 'config.json');
20
-
21
- if (!fs.existsSync(settingsFile) && !fs.existsSync(configFile)) {
22
- console.error('❌ Claude Code not found. Please install Claude Code first.');
23
- console.error(' Visit: https://claude.ai/code');
24
- process.exit(1);
25
- }
26
- console.log('✅ Claude Code detected');
27
- }
28
-
29
- function createPluginDirectory() {
30
- if (!fs.existsSync(PLUGINS_DIR)) {
31
- fs.mkdirSync(PLUGINS_DIR, { recursive: true });
32
- }
33
-
34
- if (fs.existsSync(PLUGIN_DIR)) {
35
- console.log('⚠️ Hardstop already installed. Updating...');
36
- fs.rmSync(PLUGIN_DIR, { recursive: true, force: true });
37
- }
38
-
39
- fs.mkdirSync(PLUGIN_DIR, { recursive: true });
40
- console.log('✅ Plugin directory created');
41
- }
42
-
43
- function copyPluginFiles() {
44
- // Determine source directory
45
- // When installed via npm, __dirname points to node_modules/hardstop/bin
46
- // When run from repo, __dirname points to repo/bin
47
- const sourceDir = path.dirname(__dirname);
48
-
49
- const filesToCopy = [
50
- { name: '.claude-plugin', type: 'dir' },
51
- { name: 'hooks', type: 'dir' },
52
- { name: 'commands', type: 'dir' },
53
- { name: 'patterns', type: 'dir' },
54
- { name: 'LICENSE', type: 'file' },
55
- { name: 'README.md', type: 'file' },
56
- ];
57
-
58
- for (const item of filesToCopy) {
59
- const source = path.join(sourceDir, item.name);
60
- const dest = path.join(PLUGIN_DIR, item.name);
61
-
62
- if (!fs.existsSync(source)) {
63
- console.log(`⚠️ Skipping ${item.name} (not found)`);
64
- continue;
65
- }
66
-
67
- if (item.type === 'dir') {
68
- fs.cpSync(source, dest, { recursive: true });
69
- } else {
70
- fs.copyFileSync(source, dest);
71
- }
72
- console.log(`✅ Copied ${item.name}`);
73
- }
74
- }
75
-
76
- function setExecutablePermissions() {
77
- if (os.platform() === 'win32') {
78
- console.log('ℹ️ Windows detected - skipping chmod');
79
- return;
80
- }
81
-
82
- const hookFiles = [
83
- path.join(PLUGIN_DIR, 'hooks', 'pre_tool_use.py'),
84
- path.join(PLUGIN_DIR, 'hooks', 'pre_read.py'),
85
- ];
86
-
87
- for (const file of hookFiles) {
88
- if (fs.existsSync(file)) {
89
- fs.chmodSync(file, '755');
90
- }
91
- }
92
- console.log(' Set executable permissions');
93
- }
94
-
95
- function createSkill() {
96
- if (!fs.existsSync(SKILLS_DIR)) {
97
- fs.mkdirSync(SKILLS_DIR, { recursive: true });
98
- }
99
-
100
- if (fs.existsSync(SKILL_DIR)) {
101
- fs.rmSync(SKILL_DIR, { recursive: true, force: true });
102
- }
103
-
104
- fs.mkdirSync(SKILL_DIR, { recursive: true });
105
-
106
- // Copy SKILL.md from source if available, otherwise generate inline
107
- const sourceDir = path.dirname(__dirname);
108
- const sourceSkill = path.join(sourceDir, 'skills', PLUGIN_NAME, 'SKILL.md');
109
-
110
- if (fs.existsSync(sourceSkill)) {
111
- fs.copyFileSync(sourceSkill, path.join(SKILL_DIR, 'SKILL.md'));
112
- } else {
113
- // Fallback: generate a minimal skill file
114
- const skillContent = `---
115
- name: hs
116
- version: 1.0.0
117
- description: >
118
- Hardstop - Pre-execution safety layer control. Use this skill when the user wants to
119
- enable, disable, check status, skip, or view logs for the Hardstop safety system.
120
- triggers:
121
- - hs
122
- - hs on
123
- - hs off
124
- - hs status
125
- - hs skip
126
- - hs log
127
- ---
128
-
129
- # Hardstop Control
130
-
131
- **Purpose:** Control the Hardstop pre-execution safety layer that blocks dangerous shell commands.
132
-
133
- When the user invokes \`/hs\` (with optional subcommands), run the appropriate Python command:
134
-
135
- - \`/hs\` or \`/hs status\`: \`python ~/.claude/plugins/hs/commands/hs_cmd.py status\`
136
- - \`/hs on\`: \`python ~/.claude/plugins/hs/commands/hs_cmd.py on\`
137
- - \`/hs off\`: \`python ~/.claude/plugins/hs/commands/hs_cmd.py off\`
138
- - \`/hs skip\`: \`python ~/.claude/plugins/hs/commands/hs_cmd.py skip\`
139
- - \`/hs log\`: \`python ~/.claude/plugins/hs/commands/hs_cmd.py log\`
140
- `;
141
- fs.writeFileSync(path.join(SKILL_DIR, 'SKILL.md'), skillContent, 'utf8');
142
- }
143
-
144
- console.log('✅ Skill created');
145
- }
146
-
147
- function configureHooks() {
148
- let settings = {};
149
-
150
- if (fs.existsSync(SETTINGS_FILE)) {
151
- try {
152
- const content = fs.readFileSync(SETTINGS_FILE, 'utf8').trim();
153
- settings = content ? JSON.parse(content) : {};
154
- } catch (e) {
155
- settings = {};
156
- }
157
-
158
- // Check if hooks are already configured
159
- const raw = fs.readFileSync(SETTINGS_FILE, 'utf8');
160
- if (raw.includes('pre_tool_use.py') && raw.includes('pre_read.py')) {
161
- console.log('⚠️ Hooks already configured, skipping');
162
- return;
163
- }
164
-
165
- // Backup existing settings
166
- fs.copyFileSync(SETTINGS_FILE, SETTINGS_FILE + '.backup');
167
- console.log('ℹ️ Backed up settings.json');
168
- }
169
-
170
- if (!settings.hooks) {
171
- settings.hooks = {};
172
- }
173
- if (!settings.hooks.PreToolUse) {
174
- settings.hooks.PreToolUse = [];
175
- }
176
-
177
- const bashHook = path.join(PLUGIN_DIR, 'hooks', 'pre_tool_use.py').replace(/\\/g, '/');
178
- const readHook = path.join(PLUGIN_DIR, 'hooks', 'pre_read.py').replace(/\\/g, '/');
179
-
180
- settings.hooks.PreToolUse.push({
181
- matcher: 'Bash',
182
- hooks: [{
183
- type: 'command',
184
- command: `python ${bashHook}`,
185
- timeout: 30
186
- }]
187
- });
188
-
189
- settings.hooks.PreToolUse.push({
190
- matcher: 'Read',
191
- hooks: [{
192
- type: 'command',
193
- command: `python ${readHook}`,
194
- timeout: 30
195
- }]
196
- });
197
-
198
- fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8');
199
- console.log('✅ Hooks configured (Bash + Read)');
200
- }
201
-
202
- function verifyInstallation() {
203
- const requiredFiles = [
204
- '.claude-plugin/plugin.json',
205
- 'hooks/hooks.json',
206
- 'hooks/pre_tool_use.py',
207
- 'hooks/pre_read.py',
208
- 'patterns/dangerous_commands.yaml',
209
- ];
210
-
211
- let allPresent = true;
212
- for (const file of requiredFiles) {
213
- const fullPath = path.join(PLUGIN_DIR, file);
214
- if (!fs.existsSync(fullPath)) {
215
- console.error(`❌ Missing: ${file}`);
216
- allPresent = false;
217
- }
218
- }
219
-
220
- if (allPresent) {
221
- console.log('✅ Installation verified');
222
- } else {
223
- console.error('❌ Installation incomplete');
224
- process.exit(1);
225
- }
226
- }
227
-
228
- function printSuccess() {
229
- const version = getVersion();
230
- console.log(`
231
- ╔═══════════════════════════════════════════════════════════╗
232
- 🛡️ Hardstop ${version} installed successfully! ║
233
- ╚═══════════════════════════════════════════════════════════╝
234
-
235
- Next steps:
236
- 1. Restart Claude Code (if running)
237
- 2. Test with: /hs status
238
- 3. Read docs: ${PLUGIN_DIR}/README.md
239
-
240
- Hardstop will now intercept dangerous commands before execution.
241
- Use '/hs help' to see all available commands.
242
-
243
- Features:
244
- 262 patterns mapped to MITRE ATT&CK framework
245
- • Risk scoring system with session tracking
246
- • Command chain analysis (&&, ||, ;, |)
247
- Read tool protection (credential files)
248
- Fail-closed by default
249
-
250
- Documentation:
251
- • https://github.com/frmoretto/hardstop
252
- ~/.claude/plugins/hs/README.md
253
- `);
254
- }
255
-
256
- function getVersion() {
257
- try {
258
- const pluginJson = path.join(PLUGIN_DIR, '.claude-plugin', 'plugin.json');
259
- if (fs.existsSync(pluginJson)) {
260
- const data = JSON.parse(fs.readFileSync(pluginJson, 'utf8'));
261
- return data.version || 'unknown';
262
- }
263
- } catch (e) {
264
- // Ignore errors
265
- }
266
- return 'v1.4.3';
267
- }
268
-
269
- // Main installation flow
270
- function main() {
271
- // Check for help flag
272
- if (process.argv.includes('--help') || process.argv.includes('-h')) {
273
- console.log(`
274
- Hardstop Installer
275
-
276
- Usage:
277
- npx hardstop install Install Hardstop plugin
278
- npx hardstop --help Show this help
279
-
280
- Installation:
281
- Installs Hardstop to: ~/.claude/plugins/hs
282
- Requires: Claude Code installed
283
-
284
- More info:
285
- https://github.com/frmoretto/hardstop
286
- `);
287
- process.exit(0);
288
- }
289
-
290
- console.log('\n🚀 Installing Hardstop...\n');
291
-
292
- try {
293
- detectClaude();
294
- createPluginDirectory();
295
- copyPluginFiles();
296
- setExecutablePermissions();
297
- createSkill();
298
- configureHooks();
299
- verifyInstallation();
300
- printSuccess();
301
- } catch (error) {
302
- console.error(' Installation failed:', error.message);
303
- if (process.env.DEBUG) {
304
- console.error(error.stack);
305
- }
306
- process.exit(1);
307
- }
308
- }
309
-
310
- main();
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+
7
+ const PLUGIN_NAME = 'hs';
8
+ let CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || '';
9
+ if (CLAUDE_DIR === '~') {
10
+ CLAUDE_DIR = os.homedir();
11
+ } else if (CLAUDE_DIR.startsWith('~/')) {
12
+ CLAUDE_DIR = path.join(os.homedir(), CLAUDE_DIR.slice(2));
13
+ }
14
+ CLAUDE_DIR = CLAUDE_DIR.replace(/\/+$/, '');
15
+ if (!CLAUDE_DIR) {
16
+ CLAUDE_DIR = path.join(os.homedir(), '.claude');
17
+ }
18
+ const PLUGINS_DIR = path.join(CLAUDE_DIR, 'plugins');
19
+ const PLUGIN_DIR = path.join(PLUGINS_DIR, PLUGIN_NAME);
20
+ const SKILLS_DIR = path.join(CLAUDE_DIR, 'skills');
21
+ const SKILL_DIR = path.join(SKILLS_DIR, PLUGIN_NAME);
22
+ const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
23
+
24
+ function detectClaude() {
25
+ // Check if Claude Code is installed
26
+ // Check for settings.json or config.json
27
+ const settingsFile = path.join(CLAUDE_DIR, 'settings.json');
28
+ const configFile = path.join(CLAUDE_DIR, 'config.json');
29
+
30
+ if (!fs.existsSync(settingsFile) && !fs.existsSync(configFile)) {
31
+ console.error('❌ Claude Code not found. Please install Claude Code first.');
32
+ console.error(' Visit: https://claude.ai/code');
33
+ process.exit(1);
34
+ }
35
+ console.log(' Claude Code detected');
36
+ }
37
+
38
+ function createPluginDirectory() {
39
+ if (!fs.existsSync(PLUGINS_DIR)) {
40
+ fs.mkdirSync(PLUGINS_DIR, { recursive: true });
41
+ }
42
+
43
+ if (fs.existsSync(PLUGIN_DIR)) {
44
+ console.log('⚠️ Hardstop already installed. Updating...');
45
+ fs.rmSync(PLUGIN_DIR, { recursive: true, force: true });
46
+ }
47
+
48
+ fs.mkdirSync(PLUGIN_DIR, { recursive: true });
49
+ console.log('✅ Plugin directory created');
50
+ }
51
+
52
+ function copyPluginFiles() {
53
+ // Determine source directory
54
+ // When installed via npm, __dirname points to node_modules/hardstop/bin
55
+ // When run from repo, __dirname points to repo/bin
56
+ const sourceDir = path.dirname(__dirname);
57
+
58
+ const filesToCopy = [
59
+ { name: '.claude-plugin', type: 'dir' },
60
+ { name: 'hooks', type: 'dir' },
61
+ { name: 'commands', type: 'dir' },
62
+ { name: 'patterns', type: 'dir' },
63
+ { name: 'LICENSE', type: 'file' },
64
+ { name: 'README.md', type: 'file' },
65
+ ];
66
+
67
+ for (const item of filesToCopy) {
68
+ const source = path.join(sourceDir, item.name);
69
+ const dest = path.join(PLUGIN_DIR, item.name);
70
+
71
+ if (!fs.existsSync(source)) {
72
+ console.log(`⚠️ Skipping ${item.name} (not found)`);
73
+ continue;
74
+ }
75
+
76
+ if (item.type === 'dir') {
77
+ fs.cpSync(source, dest, { recursive: true });
78
+ } else {
79
+ fs.copyFileSync(source, dest);
80
+ }
81
+ console.log(`✅ Copied ${item.name}`);
82
+ }
83
+ }
84
+
85
+ function setExecutablePermissions() {
86
+ if (os.platform() === 'win32') {
87
+ console.log('ℹ️ Windows detected - skipping chmod');
88
+ return;
89
+ }
90
+
91
+ const hookFiles = [
92
+ path.join(PLUGIN_DIR, 'hooks', 'pre_tool_use.py'),
93
+ path.join(PLUGIN_DIR, 'hooks', 'pre_read.py'),
94
+ ];
95
+
96
+ for (const file of hookFiles) {
97
+ if (fs.existsSync(file)) {
98
+ fs.chmodSync(file, '755');
99
+ }
100
+ }
101
+ console.log('✅ Set executable permissions');
102
+ }
103
+
104
+ function createSkill() {
105
+ if (!fs.existsSync(SKILLS_DIR)) {
106
+ fs.mkdirSync(SKILLS_DIR, { recursive: true });
107
+ }
108
+
109
+ if (fs.existsSync(SKILL_DIR)) {
110
+ fs.rmSync(SKILL_DIR, { recursive: true, force: true });
111
+ }
112
+
113
+ fs.mkdirSync(SKILL_DIR, { recursive: true });
114
+
115
+ // Copy SKILL.md from source if available, otherwise generate inline
116
+ const sourceDir = path.dirname(__dirname);
117
+ const sourceSkill = path.join(sourceDir, 'skills', PLUGIN_NAME, 'SKILL.md');
118
+
119
+ if (fs.existsSync(sourceSkill)) {
120
+ // Replace hardcoded ~/.claude/plugins/hs/ paths with actual install location
121
+ let content = fs.readFileSync(sourceSkill, 'utf8');
122
+ content = content.replace(/~\/\.claude\/plugins\/hs\//g, PLUGIN_DIR + '/');
123
+ fs.writeFileSync(path.join(SKILL_DIR, 'SKILL.md'), content, 'utf8');
124
+ } else {
125
+ // Fallback: generate a minimal skill file
126
+ const skillContent = `---
127
+ name: hs
128
+ version: 1.0.0
129
+ description: >
130
+ Hardstop - Pre-execution safety layer control. Use this skill when the user wants to
131
+ enable, disable, check status, skip, or view logs for the Hardstop safety system.
132
+ triggers:
133
+ - hs
134
+ - hs on
135
+ - hs off
136
+ - hs status
137
+ - hs skip
138
+ - hs log
139
+ ---
140
+
141
+ # Hardstop Control
142
+
143
+ **Purpose:** Control the Hardstop pre-execution safety layer that blocks dangerous shell commands.
144
+
145
+ When the user invokes \`/hs\` (with optional subcommands), run the appropriate Python command:
146
+
147
+ - \`/hs\` or \`/hs status\`: \`python ${PLUGIN_DIR}/commands/hs_cmd.py status\`
148
+ - \`/hs on\`: \`python ${PLUGIN_DIR}/commands/hs_cmd.py on\`
149
+ - \`/hs off\`: \`python ${PLUGIN_DIR}/commands/hs_cmd.py off\`
150
+ - \`/hs skip\`: \`python ${PLUGIN_DIR}/commands/hs_cmd.py skip\`
151
+ - \`/hs log\`: \`python ${PLUGIN_DIR}/commands/hs_cmd.py log\`
152
+ `;
153
+ fs.writeFileSync(path.join(SKILL_DIR, 'SKILL.md'), skillContent, 'utf8');
154
+ }
155
+
156
+ console.log('✅ Skill created');
157
+ }
158
+
159
+ function configureHooks() {
160
+ let settings = {};
161
+
162
+ if (fs.existsSync(SETTINGS_FILE)) {
163
+ try {
164
+ const content = fs.readFileSync(SETTINGS_FILE, 'utf8').trim();
165
+ settings = content ? JSON.parse(content) : {};
166
+ } catch (e) {
167
+ settings = {};
168
+ }
169
+
170
+ // Check if hooks are already configured
171
+ const raw = fs.readFileSync(SETTINGS_FILE, 'utf8');
172
+ if (raw.includes('pre_tool_use.py') && raw.includes('pre_read.py')) {
173
+ console.log('⚠️ Hooks already configured, skipping');
174
+ return;
175
+ }
176
+
177
+ // Backup existing settings
178
+ fs.copyFileSync(SETTINGS_FILE, SETTINGS_FILE + '.backup');
179
+ console.log('ℹ️ Backed up settings.json');
180
+ }
181
+
182
+ if (!settings.hooks) {
183
+ settings.hooks = {};
184
+ }
185
+ if (!settings.hooks.PreToolUse) {
186
+ settings.hooks.PreToolUse = [];
187
+ }
188
+
189
+ const bashHook = path.join(PLUGIN_DIR, 'hooks', 'pre_tool_use.py').replace(/\\/g, '/');
190
+ const readHook = path.join(PLUGIN_DIR, 'hooks', 'pre_read.py').replace(/\\/g, '/');
191
+
192
+ settings.hooks.PreToolUse.push({
193
+ matcher: 'Bash',
194
+ hooks: [{
195
+ type: 'command',
196
+ command: `python ${bashHook}`,
197
+ timeout: 30
198
+ }]
199
+ });
200
+
201
+ settings.hooks.PreToolUse.push({
202
+ matcher: 'Read',
203
+ hooks: [{
204
+ type: 'command',
205
+ command: `python ${readHook}`,
206
+ timeout: 30
207
+ }]
208
+ });
209
+
210
+ fs.writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf8');
211
+ console.log('✅ Hooks configured (Bash + Read)');
212
+ }
213
+
214
+ function verifyInstallation() {
215
+ const requiredFiles = [
216
+ '.claude-plugin/plugin.json',
217
+ 'hooks/hooks.json',
218
+ 'hooks/pre_tool_use.py',
219
+ 'hooks/pre_read.py',
220
+ 'patterns/dangerous_commands.yaml',
221
+ ];
222
+
223
+ let allPresent = true;
224
+ for (const file of requiredFiles) {
225
+ const fullPath = path.join(PLUGIN_DIR, file);
226
+ if (!fs.existsSync(fullPath)) {
227
+ console.error(`❌ Missing: ${file}`);
228
+ allPresent = false;
229
+ }
230
+ }
231
+
232
+ if (allPresent) {
233
+ console.log('✅ Installation verified');
234
+ } else {
235
+ console.error('❌ Installation incomplete');
236
+ process.exit(1);
237
+ }
238
+ }
239
+
240
+ function printSuccess() {
241
+ const version = getVersion();
242
+ console.log(`
243
+ ╔═══════════════════════════════════════════════════════════╗
244
+ 🛡️ Hardstop ${version} installed successfully! ║
245
+ ╚═══════════════════════════════════════════════════════════╝
246
+
247
+ Next steps:
248
+ 1. Restart Claude Code (if running)
249
+ 2. Test with: /hs status
250
+ 3. Read docs: ${PLUGIN_DIR}/README.md
251
+
252
+ Hardstop will now intercept dangerous commands before execution.
253
+ Use '/hs help' to see all available commands.
254
+
255
+ Features:
256
+ 262 patterns mapped to MITRE ATT&CK framework
257
+ Risk scoring system with session tracking
258
+ Command chain analysis (&&, ||, ;, |)
259
+ Read tool protection (credential files)
260
+ Fail-closed by default
261
+
262
+ Documentation:
263
+ https://github.com/frmoretto/hardstop
264
+ ${PLUGIN_DIR}/README.md
265
+ `);
266
+ }
267
+
268
+ function getVersion() {
269
+ try {
270
+ const pluginJson = path.join(PLUGIN_DIR, '.claude-plugin', 'plugin.json');
271
+ if (fs.existsSync(pluginJson)) {
272
+ const data = JSON.parse(fs.readFileSync(pluginJson, 'utf8'));
273
+ return data.version || 'unknown';
274
+ }
275
+ } catch (e) {
276
+ // Ignore errors
277
+ }
278
+ return 'v1.4.3';
279
+ }
280
+
281
+ // Main installation flow
282
+ function main() {
283
+ // Check for help flag
284
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
285
+ console.log(`
286
+ Hardstop Installer
287
+
288
+ Usage:
289
+ npx hardstop install Install Hardstop plugin
290
+ npx hardstop --help Show this help
291
+
292
+ Installation:
293
+ Installs Hardstop to: ${CLAUDE_DIR}/plugins/hs
294
+ Requires: Claude Code installed
295
+
296
+ More info:
297
+ https://github.com/frmoretto/hardstop
298
+ `);
299
+ process.exit(0);
300
+ }
301
+
302
+ console.log('\n🚀 Installing Hardstop...\n');
303
+
304
+ try {
305
+ detectClaude();
306
+ createPluginDirectory();
307
+ copyPluginFiles();
308
+ setExecutablePermissions();
309
+ createSkill();
310
+ configureHooks();
311
+ verifyInstallation();
312
+ printSuccess();
313
+ } catch (error) {
314
+ console.error('❌ Installation failed:', error.message);
315
+ if (process.env.DEBUG) {
316
+ console.error(error.stack);
317
+ }
318
+ process.exit(1);
319
+ }
320
+ }
321
+
322
+ main();