slashdev 1.0.0 → 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,88 @@
1
+ #!/usr/bin/env bash
2
+ # pre-install-guard.sh — PreToolUse hook for Bash tool calls.
3
+ #
4
+ # Blocks bare dependency install commands and forces them to be re-run with
5
+ # Socket Firewall (`sfw`) prefix to protect against supply-chain attacks.
6
+ #
7
+ # Reads tool-call JSON from stdin, returns:
8
+ # {} → allow
9
+ # {"permissionDecision":"deny","message":"..."} → block
10
+ #
11
+ # Patterns matched (anchored at the start of the actual command, after any
12
+ # leading `cd ... &&` prefix):
13
+ # - npm/pnpm/yarn/bun (install|i|ci|add|update|upgrade)
14
+ # - composer (install|require|update)
15
+ # - pip/pip3/uv (install|sync)
16
+ set -euo pipefail
17
+
18
+ INPUT=$(cat)
19
+
20
+ # Extract tool_input.command from the JSON. Try grep/sed first for speed,
21
+ # fall back to python3 for properly-escaped JSON.
22
+ COMMAND=$(printf '%s' "$INPUT" \
23
+ | grep -o '"command"[[:space:]]*:[[:space:]]*"[^"]*"' \
24
+ | head -1 \
25
+ | sed 's/.*:[[:space:]]*"//;s/"$//' \
26
+ || true)
27
+
28
+ if [ -z "$COMMAND" ]; then
29
+ COMMAND=$(printf '%s' "$INPUT" \
30
+ | python3 -c 'import sys,json; print(json.loads(sys.stdin.read()).get("tool_input",{}).get("command",""))' \
31
+ 2>/dev/null || true)
32
+ fi
33
+
34
+ # No command extracted → allow (don't block on parse failure).
35
+ if [ -z "$COMMAND" ]; then
36
+ echo '{}'
37
+ exit 0
38
+ fi
39
+
40
+ # Strip leading whitespace.
41
+ TRIMMED=$(printf '%s' "$COMMAND" | sed 's/^[[:space:]]*//')
42
+
43
+ # Strip a leading `cd <dir> && ` prefix (or `cd <dir>; `) so we examine the
44
+ # actual command being run, not the directory change.
45
+ ACTUAL_CMD="$TRIMMED"
46
+ while printf '%s' "$ACTUAL_CMD" | grep -qE '^cd[[:space:]]+[^&;]+[[:space:]]*(&&|;)[[:space:]]*'; do
47
+ ACTUAL_CMD=$(printf '%s' "$ACTUAL_CMD" \
48
+ | sed -E 's/^cd[[:space:]]+[^&;]+[[:space:]]*(&&|;)[[:space:]]*//')
49
+ done
50
+
51
+ # Strip leading env-var assignments (e.g. `NODE_ENV=production npm ci`).
52
+ while printf '%s' "$ACTUAL_CMD" | grep -qE '^[A-Z_][A-Z0-9_]*=[^[:space:]]+[[:space:]]+'; do
53
+ ACTUAL_CMD=$(printf '%s' "$ACTUAL_CMD" \
54
+ | sed -E 's/^[A-Z_][A-Z0-9_]*=[^[:space:]]+[[:space:]]+//')
55
+ done
56
+
57
+ # Already wrapped with sfw → allow immediately.
58
+ if printf '%s' "$ACTUAL_CMD" | grep -qE '^sfw([[:space:]]|$)'; then
59
+ echo '{}'
60
+ exit 0
61
+ fi
62
+
63
+ # Detect install commands. Patterns are conservative — must be the first token,
64
+ # followed by an install-style subcommand.
65
+ IS_INSTALL=0
66
+ case "$ACTUAL_CMD" in
67
+ npm[[:space:]]install*|npm[[:space:]]i[[:space:]]*|"npm i"|npm[[:space:]]ci*|npm[[:space:]]add*|npm[[:space:]]update*|npm[[:space:]]upgrade*) IS_INSTALL=1 ;;
68
+ pnpm[[:space:]]install*|pnpm[[:space:]]i[[:space:]]*|"pnpm i"|pnpm[[:space:]]add*|pnpm[[:space:]]update*|pnpm[[:space:]]upgrade*) IS_INSTALL=1 ;;
69
+ yarn[[:space:]]install*|yarn[[:space:]]add*|yarn[[:space:]]upgrade*|"yarn") IS_INSTALL=1 ;;
70
+ bun[[:space:]]install*|bun[[:space:]]i[[:space:]]*|"bun i"|bun[[:space:]]add*|bun[[:space:]]update*) IS_INSTALL=1 ;;
71
+ composer[[:space:]]install*|composer[[:space:]]require*|composer[[:space:]]update*|composer[[:space:]]upgrade*) IS_INSTALL=1 ;;
72
+ pip[[:space:]]install*|pip3[[:space:]]install*|uv[[:space:]]install*|uv[[:space:]]sync*|uv[[:space:]]add*) IS_INSTALL=1 ;;
73
+ esac
74
+
75
+ if [ "$IS_INSTALL" -eq 0 ]; then
76
+ echo '{}'
77
+ exit 0
78
+ fi
79
+
80
+ # It's an install command without sfw — block it.
81
+ # Escape the original command for safe inclusion in JSON.
82
+ ESCAPED_CMD=$(printf '%s' "$ACTUAL_CMD" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read())[1:-1])' 2>/dev/null || printf '%s' "$ACTUAL_CMD" | sed 's/\\/\\\\/g; s/"/\\"/g')
83
+
84
+ if command -v sfw >/dev/null 2>&1; then
85
+ printf '{"permissionDecision":"deny","message":"[slashdev] Bare install command blocked. Re-run with the sfw prefix:\\n\\n sfw %s\\n\\nSocket Firewall scans every package against Socket'"'"'s vulnerability/malware database before installing it. This protects against supply-chain attacks."}\n' "$ESCAPED_CMD"
86
+ else
87
+ printf '{"permissionDecision":"deny","message":"[slashdev] Socket Firewall (sfw) is required for dependency installs but is not installed on this machine. Install it once:\\n\\n npm install -g @socketsecurity/cli\\n\\nThen retry the install with:\\n\\n sfw %s"}\n' "$ESCAPED_CMD"
88
+ fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slashdev",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Skills & workflows for AI agents in the Slashdev ecosystem",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -20,6 +20,38 @@ const CLAUDE_DIR = join(homedir(), '.claude');
20
20
  const CLAUDE_HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
21
21
  const CLAUDE_SKILLS_DIR = join(CLAUDE_DIR, 'skills');
22
22
  const CLAUDE_SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
23
+ const CLAUDE_MEMORY_FILE = join(CLAUDE_DIR, 'CLAUDE.md');
24
+
25
+ const SLASHDEV_SECTION_START = '<!-- SLASHDEV:RULES:START -->';
26
+ const SLASHDEV_SECTION_END = '<!-- SLASHDEV:RULES:END -->';
27
+
28
+ const SLASHDEV_RULES_BLOCK = `${SLASHDEV_SECTION_START}
29
+ # Slashdev workspace rules
30
+
31
+ ## Security: always use Socket Firewall for dependency installs
32
+
33
+ Claude must use Socket Firewall (\`sfw\`) for **every** dependency install command.
34
+ This applies to:
35
+
36
+ - **Local development** — never run \`npm install\`, \`pnpm install\`, \`yarn add\`,
37
+ \`bun install\`, \`composer install\`, \`pip install\`, or any equivalent without
38
+ prefixing \`sfw \`. A PreToolUse hook will block bare install commands and tell
39
+ you to retry with \`sfw\`.
40
+ - **Production deployment** — when generating or editing CI/CD config (GitHub Actions,
41
+ GitLab CI, CircleCI, Jenkins), Dockerfiles, deploy scripts (\`deploy.sh\`, \`Procfile\`),
42
+ or \`package.json\` scripts that install dependencies, **always** wrap the install
43
+ step with \`sfw\`. Examples:
44
+ - \`sfw npm ci\` instead of \`npm ci\`
45
+ - \`sfw composer install --no-dev\` instead of \`composer install --no-dev\`
46
+ - \`RUN npm install -g @socketsecurity/cli && sfw npm ci\` in a Dockerfile
47
+ - If \`sfw\` is not yet installed in a CI environment, install it first:
48
+ \`npm install -g @socketsecurity/cli\`. Document the install step inline in the workflow.
49
+ - Use the \`/secure-deploy\` skill to retrofit existing CI/CD configs.
50
+
51
+ Reason: Socket Firewall scans every package against Socket's vulnerability/malware
52
+ database before letting it install. This protects against supply-chain attacks
53
+ (compromised packages, typosquats, dependency confusion).
54
+ ${SLASHDEV_SECTION_END}`;
23
55
 
24
56
  function ensureDir(dir) {
25
57
  if (!existsSync(dir)) {
@@ -87,12 +119,64 @@ export function installHooks() {
87
119
  settings.hooks.Stop = stopHooks;
88
120
  }
89
121
 
122
+ // Register PreToolUse hook for the install guard (Bash tool only).
123
+ const guardCommand = join(CLAUDE_HOOKS_DIR, 'pre-install-guard.sh');
124
+ if (existsSync(guardCommand)) {
125
+ const preToolUseHooks = settings.hooks.PreToolUse || [];
126
+ const guardRegistered = preToolUseHooks.some(
127
+ (entry) =>
128
+ entry.matcher === 'Bash' &&
129
+ Array.isArray(entry.hooks) &&
130
+ entry.hooks.some((h) => h.command && h.command.includes('pre-install-guard.sh'))
131
+ );
132
+
133
+ if (!guardRegistered) {
134
+ preToolUseHooks.push({
135
+ matcher: 'Bash',
136
+ hooks: [
137
+ {
138
+ type: 'command',
139
+ command: guardCommand,
140
+ statusMessage: 'Checking install safety…',
141
+ },
142
+ ],
143
+ });
144
+ settings.hooks.PreToolUse = preToolUseHooks;
145
+ }
146
+ }
147
+
90
148
  writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
91
149
 
92
150
  const hookFiles = readdirSync(hooksSource).filter((f) => f.endsWith('.sh'));
93
151
  return { installed: hookFiles.length };
94
152
  }
95
153
 
154
+ export function writeGlobalClaudeMd() {
155
+ ensureDir(CLAUDE_DIR);
156
+
157
+ let existing = '';
158
+ if (existsSync(CLAUDE_MEMORY_FILE)) {
159
+ existing = readFileSync(CLAUDE_MEMORY_FILE, 'utf-8');
160
+ }
161
+
162
+ let next;
163
+ if (existing.includes(SLASHDEV_SECTION_START) && existing.includes(SLASHDEV_SECTION_END)) {
164
+ // Replace the existing managed section in place, preserving everything around it.
165
+ const before = existing.split(SLASHDEV_SECTION_START)[0];
166
+ const after = existing.split(SLASHDEV_SECTION_END)[1];
167
+ next = `${before}${SLASHDEV_RULES_BLOCK}${after}`;
168
+ } else if (existing.trim().length > 0) {
169
+ // Append after existing user content.
170
+ next = `${existing.trimEnd()}\n\n${SLASHDEV_RULES_BLOCK}\n`;
171
+ } else {
172
+ // Brand new file.
173
+ next = `${SLASHDEV_RULES_BLOCK}\n`;
174
+ }
175
+
176
+ writeFileSync(CLAUDE_MEMORY_FILE, next, 'utf-8');
177
+ return { written: true, path: CLAUDE_MEMORY_FILE };
178
+ }
179
+
96
180
  export function installSkills() {
97
181
  const skillsSource = join(PACKAGE_ROOT, 'claude-skills');
98
182
  if (!existsSync(skillsSource)) {
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url';
6
6
  import { existsSync, readFileSync, mkdirSync } from 'fs';
7
7
  import { homedir } from 'os';
8
8
  import { getConfig, setConfig, getSlashdevDir } from '../core/config.js';
9
- import { installHooks, installSkills, installGstack } from './setup-internals.js';
9
+ import { installHooks, installSkills, installGstack, writeGlobalClaudeMd } from './setup-internals.js';
10
10
 
11
11
  const __filename = fileURLToPath(import.meta.url);
12
12
  const __dirname = dirname(__filename);
@@ -82,6 +82,10 @@ export async function setup() {
82
82
  spinner.text = 'Installing gstack skills...';
83
83
  const gstackResult = installGstack();
84
84
 
85
+ // Write global Claude rules (Socket Firewall + workspace policy)
86
+ spinner.text = 'Writing global Claude rules...';
87
+ const rulesResult = writeGlobalClaudeMd();
88
+
85
89
  spinner.succeed('Slashdev Claude tools installed');
86
90
 
87
91
  console.log('');
@@ -92,9 +96,11 @@ export async function setup() {
92
96
  if (gstackResult.warning) {
93
97
  console.log(` ${chalk.yellow('!')} ${gstackResult.warning}`);
94
98
  }
99
+ console.log(` ${chalk.green('✓')} Rules: ${rulesResult.path}`);
95
100
  console.log(` ${chalk.green('✓')} Config: ${join(getSlashdevDir(), 'config.json')}`);
96
101
  console.log('');
97
- console.log(chalk.dim(' Hooks and skills have been installed to ~/.claude/'));
102
+ console.log(chalk.dim(' Hooks, skills, and global rules have been installed to ~/.claude/'));
103
+ console.log(chalk.dim(' Socket Firewall enforcement is active for all Bash install commands.'));
98
104
  console.log(chalk.dim(' Prompt logs will be sent to the Slashdev API for quality review.\n'));
99
105
  } catch (err) {
100
106
  spinner.fail('Setup failed');
@@ -27,7 +27,7 @@ export async function update() {
27
27
  try {
28
28
  // Re-import setup internals by calling setup's install functions
29
29
  // We import dynamically to avoid circular deps
30
- const { installHooks, installSkills, installGstack } = await import('./setup-internals.js');
30
+ const { installHooks, installSkills, installGstack, writeGlobalClaudeMd } = await import('./setup-internals.js');
31
31
 
32
32
  const hookResult = installHooks();
33
33
 
@@ -37,6 +37,9 @@ export async function update() {
37
37
  spinner.text = 'Updating gstack skills...';
38
38
  const gstackResult = installGstack();
39
39
 
40
+ spinner.text = 'Updating global Claude rules...';
41
+ const rulesResult = writeGlobalClaudeMd();
42
+
40
43
  // Update version in config
41
44
  const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf-8'));
42
45
  setConfig('packageVersion', pkg.version);
@@ -50,6 +53,7 @@ export async function update() {
50
53
  if (gstackResult.warning) {
51
54
  console.log(` ${chalk.yellow('!')} ${gstackResult.warning}`);
52
55
  }
56
+ console.log(` ${chalk.green('✓')} Rules: ${rulesResult.path}`);
53
57
  console.log(` ${chalk.green('✓')} Config: version set to ${pkg.version}`);
54
58
  console.log('');
55
59
  } catch (err) {