Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/agents/security-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ Apply these rules from CLAUDE.md exactly:

**Safe File Operations**: Use safeDelete()/safeDeleteSync() from @socketsecurity/lib/fs. NEVER fs.rm(), fs.rmSync(), or rm -rf. Use os.tmpdir() + fs.mkdtemp() for temp dirs. NEVER use fetch() β€” use httpJson/httpText/httpRequest from @socketsecurity/lib/http-request.

**Absolute Rules**: NEVER use npx, pnpm dlx, or yarn dlx. Use pnpm exec or pnpm run with pinned devDeps.
**Absolute Rules**: NEVER use npx, pnpm dlx, or yarn dlx. Use pnpm exec or pnpm run with pinned devDeps. # zizmor: documentation-prohibition

**Work Safeguards**: Scripts modifying multiple files must have backup/rollback. Git operations that rewrite history require explicit confirmation.

**Review checklist:**

1. **Secrets**: Hardcoded API keys, passwords, tokens, private keys in code or config
2. **Injection**: Command injection via shell: true or string interpolation in spawn/exec. Path traversal in file operations.
3. **Dependencies**: npx/dlx usage. Unpinned versions (^ or ~). Missing minimumReleaseAge bypass justification.
3. **Dependencies**: npx/dlx usage. Unpinned versions (^ or ~). Missing minimumReleaseAge bypass justification. # zizmor: documentation-checklist
4. **File operations**: fs.rm without safeDelete. process.chdir usage. fetch() usage (must use lib's httpRequest).
5. **GitHub Actions**: Unpinned action versions (must use full SHA). Secrets outside env blocks. Template injection from untrusted inputs.
6. **Error handling**: Sensitive data in error messages. Stack traces exposed to users.
Expand Down
12 changes: 8 additions & 4 deletions .claude/hooks/check-new-deps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ When Claude edits a file like `package.json`, `requirements.txt`, `Cargo.toml`,

1. **Detects the file type** and extracts dependency names from the content
2. **Diffs against the old content** (for edits) so only *newly added* deps are checked
3. **Queries the Socket.dev API** to check for malware
4. **Blocks the edit** (exit code 2) if malware is detected
5. **Allows** (exit code 0) if everything is clean or the file isn't a manifest
3. **Queries the Socket.dev API** to check for malware and critical security alerts
4. **Blocks the edit** (exit code 2) if malware or critical alerts are found
5. **Warns** (but allows) if a package has a low quality score
6. **Allows** (exit code 0) if everything is clean or the file isn't a manifest

## How it works

Expand All @@ -29,8 +30,11 @@ Build Package URLs (PURLs) for each dep
β”‚
β–Ό
Call sdk.checkMalware(components)
- ≀5 deps: parallel firewall API (fast, full data)
- >5 deps: batch PURL API (efficient)
β”‚
β”œβ”€β”€ Malware detected β†’ EXIT 2 (blocked)
β”œβ”€β”€ Malware/critical alert β†’ EXIT 2 (blocked)
β”œβ”€β”€ Low score β†’ warn, EXIT 0 (allowed)
└── Clean β†’ EXIT 0 (allowed)
```

Expand Down
80 changes: 56 additions & 24 deletions .claude/hooks/check-new-deps/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ interface CheckResult {
reason?: string
}


// A cached API lookup result with expiration timestamp.
interface CacheEntry {
result: CheckResult | undefined
Expand Down Expand Up @@ -159,23 +160,46 @@ const extractors: Record<string, Extractor> = {
(m): Dep => ({ type: 'cargo', name: m[1] })
),
'Cargo.toml': (content: string): Dep[] => {
// Rust: only extract from [dependencies], [dev-dependencies], [build-dependencies] sections.
// Skip [package], [lib], [bin], [workspace], [profile] metadata sections.
// Rust: extract crate names from dep lines.
//
// Two-mode strategy because the hook receives either a full
// Cargo.toml (Write) or a fragment (Edit's new_string, often just
// the added line with no section header):
//
// Full file β€” scan only [dependencies] / [dev-dependencies] /
// [build-dependencies] (incl. target-specific
// [target.*.dependencies] via the `.<name>` suffix)
// and skip [package], [features], [profile], etc.
// Fragment β€” no section headers at all β†’ treat the whole
// content as an implicit [dependencies] body and
// match any `name = "..."` or `name = { version = "..." }`.
//
// The lineRe requires the value to look like a version spec
// (string or table with a `version` key), so `[features]`-style
// `key = ["derive"]` array values don't match even in fragment mode.
const deps: Dep[] = []
const depSectionRe = /^\[(?:(?:dev-|build-)?dependencies(?:\.[^\]]+)?)\]\s*$/gm
const depSectionRe = /^\[(?:(?:dev-|build-)?dependencies(?:\.[^\]]+)?|target\.[^\]]+\.(?:dev-|build-)?dependencies(?:\.[^\]]+)?)\]\s*$/gm
const anySectionRe = /^\[/gm
const lineRe = /^(\w[\w-]*)\s*=\s*(?:\{[^}]*version\s*=\s*"[^"]*"|\s*"[^"]*")/gm
const push = (section: string) => {
let m
while ((m = lineRe.exec(section)) !== null) {
deps.push({ type: 'cargo', name: m[1] })
}
lineRe.lastIndex = 0
}
const hasAnySection = /^\[/m.test(content)
if (!hasAnySection) {
push(content)
return deps
}
let sectionMatch
while ((sectionMatch = depSectionRe.exec(content)) !== null) {
const sectionStart = sectionMatch.index + sectionMatch[0].length
anySectionRe.lastIndex = sectionStart
const nextSection = anySectionRe.exec(content)
const sectionEnd = nextSection ? nextSection.index : content.length
const sectionText = content.slice(sectionStart, sectionEnd)
const lineRe = /^(\w[\w-]*)\s*=\s*(?:\{[^}]*version\s*=\s*"[^"]*"|\s*"[^"]*")/gm
let m
while ((m = lineRe.exec(sectionText)) !== null) {
deps.push({ type: 'cargo', name: m[1] })
}
push(content.slice(sectionStart, sectionEnd))
}
return deps
},
Expand Down Expand Up @@ -280,21 +304,6 @@ const extractors: Record<string, Extractor> = {
'yarn.lock': extractNpmLockfile,
}

// --- main (only when executed directly, not imported) ---

if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
// Read the full JSON blob from stdin (piped by Claude Code).
let input = ''
for await (const chunk of process.stdin) input += chunk
const hook: HookInput = JSON.parse(input)

if (hook.tool_name !== 'Edit' && hook.tool_name !== 'Write') {
process.exitCode = 0
} else {
process.exitCode = await check(hook)
}
}

// --- core ---

// Orchestrates the full check: extract deps, diff against old, query API.
Expand Down Expand Up @@ -728,3 +737,26 @@ export {
extractTerraform,
findExtractor,
}

// --- main (only when executed directly, not imported) ---
//
// Kept at the bottom because the module uses top-level await
// (`for await (const chunk of process.stdin)`) to read the hook payload.
// Top-level await suspends module evaluation at the suspension point, so
// any `const` declared AFTER the suspending block is still in the TDZ
// when the awaited work calls back into the module (e.g. extractNpm β†’
// PACKAGE_JSON_METADATA_KEYS). Placing main last guarantees every
// module-level declaration is initialized before main runs.

if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
// Read the full JSON blob from stdin (piped by Claude Code).
let input = ''
for await (const chunk of process.stdin) input += chunk
const hook: HookInput = JSON.parse(input)

if (hook.tool_name !== 'Edit' && hook.tool_name !== 'Write') {
process.exitCode = 0
} else {
process.exitCode = await check(hook)
}
}
4 changes: 2 additions & 2 deletions .claude/hooks/check-new-deps/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@socketsecurity/hook-check-new-deps",
"name": "hook-check-new-deps",
"private": true,
"type": "module",
"main": "./index.mts",
Expand All @@ -11,7 +11,7 @@
},
"dependencies": {
"@socketregistry/packageurl-js": "1.4.2",
"@socketsecurity/lib": "5.21.0",
"@socketsecurity/lib": "5.24.0",
"@socketsecurity/sdk": "4.0.1"
},
"devDependencies": {
Expand Down
66 changes: 66 additions & 0 deletions .claude/hooks/path-guard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# path-guard

Claude Code `PreToolUse` hook that refuses `Edit`/`Write` tool calls that would *construct* a multi-segment build/output path inline in a `.mts` or `.cts` file. Mandatory across the Socket fleet β€” every repo ships this file byte-for-byte via `scripts/sync-scaffolding.mjs`.

**Mantra: 1 path, 1 reference.**

Construct a path *once* in the canonical `paths.mts` (or a build-infra helper); reference the computed value everywhere else.

## What it blocks

| Rule | Example | Fix |
|------|---------|-----|
| **A** β€” Multi-stage path constructed inline | `path.join(PKG, 'build', mode, 'out', 'Final', name)` | Construct in the package's `scripts/paths.mts` (or use `getFinalBinaryPath` from `build-infra/lib/paths`); import the computed value here |
| **B** β€” Cross-package path traversal | `path.join(PKG, '..', 'lief-builder', 'build', ...)` | Add `lief-builder: workspace:*` as a dep; import its `paths.mts` via the workspace `exports` field |

The hook fires on `Edit` and `Write` tool calls when the target path ends in `.mts` or `.cts`. Other extensions (`.ts`, `.mjs`, `.js`, `.yml`, `.json`, `.md`) pass through β€” TS path code lives in `.mts` per CLAUDE.md, and other file types are covered by the `scripts/check-paths.mts` gate at commit time.

## What it allows

- Edits to a `paths.mts` (canonical constructor β€” every package's source of truth).
- Edits to `scripts/check-paths.mts` (the gate, which legitimately enumerates patterns).
- Edits to this hook's own files (the test suite has to enumerate the same patterns).
- Edits to `scripts/check-consistency.mts` (existing path-scanning gate).
- `path.join` calls with a single stage segment (e.g. `path.join(packageRoot, 'build', 'temp')`) β€” that's a one-off helper path, not a multi-stage build output.
- `path.join` calls with no stage segments at all (most general-purpose joins).
- Any string concatenation that doesn't go through `path.join` β€” the hook is regex-based and intentionally narrow; the gate runs a deeper scan at commit time.

## Stage segments the hook recognizes

These come from `build-infra/lib/constants.mts` `BUILD_STAGES` plus the lowercase directory-name siblings used by some builders:

`Final`, `Release`, `Stripped`, `Compressed`, `Optimized`, `Synced`, `wasm`, `downloaded`

Two or more in the same `path.join` call (or one stage + one of `'build'`/`'out'` + one mode `'dev'`/`'prod'`) triggers Rule A.

## Known sibling packages (for Rule B)

The hook recognizes Rule B traversals only when the next segment after `..` is a known fleet package name:

`binflate`, `binject`, `binpress`, `bin-infra`, `build-infra`, `codet5-models-builder`, `curl-builder`, `iocraft-builder`, `ink-builder`, `libpq-builder`, `lief-builder`, `minilm-builder`, `models`, `napi-go`, `node-smol-builder`, `onnxruntime-builder`, `opentui-builder`, `stubs-builder`, `ultraviolet-builder`, `yoga-layout-builder`

When a new package joins the workspace, add it here.

## Control flow

The hook reads the tool-use payload from stdin, type-checks `tool_name === 'Edit'` or `'Write'`, filters to `.mts`/`.cts` files, and runs `check(source)`. Any rule violation `throw`s a typed `BlockError`; a single top-level `try/catch` in `main()` writes the block message to stderr and sets `process.exitCode = 2`.

Hook bugs fail **open** β€” a crash in the hook writes a log line and returns exit 0 so legitimate work isn't blocked on a bad deploy. The companion `scripts/check-paths.mts` gate runs a thorough whole-repo scan at `pnpm check` time, catching anything the hook misses.

## Testing

```bash
pnpm --filter hook-path-guard test
```

Adding a new detection pattern: update `STAGE_SEGMENTS` (or `KNOWN_SIBLING_PACKAGES`) in `index.mts`, add a positive and negative test in `test/path-guard.test.mts`.

## Updating across the fleet

This file is in `IDENTICAL_FILES` in `scripts/sync-scaffolding.mjs` (in `socket-repo-template`). After editing, run from `socket-repo-template`:

```bash
node scripts/sync-scaffolding.mjs --all --fix
```

to propagate the change to every fleet repo.
Loading