Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
84 changes: 58 additions & 26 deletions .claude/hooks/check-new-deps/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ import {
import { SocketSdk } from '@socketsecurity/sdk'
import type { MalwareCheckPackage } from '@socketsecurity/sdk'

// Hook runs standalone with only @socketsecurity/* deps, so this
// one-liner lives here instead of importing a shared helper.
// Local mirror of build-infra/lib/error-utils#errorMessage. Hook runs
// standalone (no workspace deps beyond @socketsecurity/*) so we can't import
// the shared helper, but the contract is identical.
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
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)
}
}
2 changes: 1 addition & 1 deletion .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 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