Skip to content

Commit 6d80b91

Browse files
committed
chore(sync): fleet scaffolding cascade — oxfmt/oxlint -c config + drift
Full sync-scaffolding pass from socket-wheelhouse. Most consequential change: `oxfmt` / `oxlint` invocations in scripts/lint.mts now consistently pass `-c .config/oxfmtrc.json` / `-c .config/oxlintrc.json`. Some repos were missing the flag entirely (defaulting oxfmt to double-quotes-plus- semis, which would silently rewrite ~200 files on first run); others had the flag form `--config` followed by an empty arg, leaving oxlint without a config path. Also picks up: * package.json scripts `format` / `format:check` aligned to canonical `oxfmt -c .config/oxfmtrc.json --{write,check} .` * `.claude/hooks/*` updates (excuse-detector, no-experimental-strip- types-guard, _shared helpers — whichever the repo was missing) * `.config/oxlint-plugin/rules/*` rule definitions resynced * `.git-hooks/*`, `scripts/{fix,security,update,power-state,...}.mts`, `scripts/lockstep-emit-schema.mts`, `scripts/socket-wheelhouse-*` * `CLAUDE.md` fleet block Run `pnpm run sync-scaffolding --check` to verify clean.
1 parent aa387ee commit 6d80b91

53 files changed

Lines changed: 1957 additions & 750 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/commands/quality-loop.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
---
2+
description: Run /scanning-quality and fix all issues found, repeating until clean or 5 iterations complete
3+
---
4+
15
Run the `/scanning-quality` skill and fix all issues found. Repeat until zero issues remain or 5 iterations complete.
26

37
**Interactive only** — this command makes code changes and commits. Do not use as an automated pipeline gate.

.claude/commands/security-scan.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
---
2+
description: Chain AgentShield (AI scanner) + Zizmor (GH Actions scanner) + security-reviewer agent for a graded security report
3+
---
4+
15
Run the `/scanning-security` skill. This chains AgentShield (Claude config audit) → zizmor (GitHub Actions security) → security-reviewer agent (grading).
26

37
For a quick manual run without the full pipeline: `pnpm run security`
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* @fileoverview Shared helper for Bash-tool PreToolUse hooks.
3+
*
4+
* Hooks that inspect tool_input.command for a forbidden substring
5+
* (e.g. a destructive verb, a stale flag, a secret pattern) all face
6+
* the same false-positive risk: the match might fall inside a quoted
7+
* string body (`echo "tip: drop --bad-flag from your script"`) or
8+
* inside a heredoc that the shell will pass as literal text rather
9+
* than execute. This module centralizes the parsing so each hook can
10+
* reason in terms of "did the forbidden token appear as a real
11+
* argument" rather than "does the string contain this text."
12+
*
13+
* Two-level API:
14+
*
15+
* buildQuoteMask(s) — per-character boolean array; mask[i] === true
16+
* when the character at index i sits inside a single- or
17+
* double-quoted string. Use this when you need to check a regex
18+
* match's index against quote state.
19+
*
20+
* matchOutsideQuotes(s, re) — convenience: run a regex against `s`
21+
* and return the first match whose index sits OUTSIDE all quotes
22+
* and outside any heredoc body. Returns undefined when every
23+
* match is inside quoted/heredoc text. Use this for the common
24+
* "does the live command contain this flag" check.
25+
*
26+
* Limitations:
27+
*
28+
* - Not a full POSIX shell parser. Quote nesting (`$"..."`,
29+
* `$'...'` ANSI-C) and `$(...)` command substitution are not
30+
* tracked precisely; they fall through to the simple quote
31+
* state. In practice this is fine for the use cases here, which
32+
* all match a literal flag/verb that wouldn't appear inside
33+
* parameter expansion.
34+
*
35+
* - Heredoc detection looks for `<<DELIM ... \nDELIM\b` patterns.
36+
* The delimiter is captured from the opening line and matched on
37+
* a later line at column 0. Both `<<EOF` and `<<-EOF` (tab-stripped)
38+
* forms are recognized; quoted delimiters (`<<'EOF'`) are also
39+
* accepted.
40+
*/
41+
42+
/**
43+
* Per-character mask: true at positions inside a single- or double-
44+
* quoted string. The opening and closing quote characters themselves
45+
* are marked true (so they're treated as "inside" — handy for code
46+
* that wants to skip both the quotes and the body).
47+
*/
48+
export function buildQuoteMask(s: string): boolean[] {
49+
const mask = new Array<boolean>(s.length).fill(false)
50+
let inSingle = false
51+
let inDouble = false
52+
for (let i = 0; i < s.length; i += 1) {
53+
const c = s[i]
54+
if (!inSingle && !inDouble && c === "'") {
55+
inSingle = true
56+
mask[i] = true
57+
continue
58+
}
59+
if (inSingle && c === "'") {
60+
inSingle = false
61+
mask[i] = true
62+
continue
63+
}
64+
if (!inSingle && !inDouble && c === '"') {
65+
inDouble = true
66+
mask[i] = true
67+
continue
68+
}
69+
if (inDouble && c === '"') {
70+
inDouble = false
71+
mask[i] = true
72+
continue
73+
}
74+
// Backslash escape inside double quotes: skip the escaped char.
75+
// (Single quotes don't honor backslash in POSIX, so we only
76+
// handle the double-quote case.)
77+
if (inDouble && c === '\\' && i + 1 < s.length) {
78+
mask[i] = true
79+
mask[i + 1] = true
80+
i += 1
81+
continue
82+
}
83+
mask[i] = inSingle || inDouble
84+
}
85+
return mask
86+
}
87+
88+
/**
89+
* Replace heredoc bodies with empty strings of equivalent length so
90+
* the surrounding indices stay valid. Recognizes:
91+
* <<EOF ... \nEOF
92+
* <<-EOF ... \nEOF (tab-stripped form)
93+
* <<'EOF' ... \nEOF (quoted delimiter, no interpolation)
94+
* <<"EOF" ... \nEOF
95+
*
96+
* The closing delimiter must appear at column 0 (POSIX), but we
97+
* accept any leading whitespace as a small concession to the
98+
* tab-stripped `<<-` form.
99+
*/
100+
export function stripHeredocBodies(s: string): string {
101+
return s.replace(
102+
/<<-?\s*['"]?(\w+)['"]?([\s\S]*?)\n\s*\1\b/g,
103+
(full, _delim, body) => {
104+
// Replace the body with spaces so indices in the outer string
105+
// stay aligned. The opening line + delimiter line are kept so
106+
// callers can still see the `<<EOF` token if they care.
107+
return full.replace(body, ' '.repeat(body.length))
108+
},
109+
)
110+
}
111+
112+
/**
113+
* Search `s` for the first regex match whose index falls outside
114+
* every single-/double-quoted string AND outside every heredoc body.
115+
* Returns the match, or undefined if every match was inside quoted
116+
* or heredoc text.
117+
*
118+
* The regex is run with the `g` flag implicitly — pass a non-global
119+
* regex and we'll create a global clone so `.exec()` can iterate.
120+
*/
121+
export function matchOutsideQuotes(
122+
s: string,
123+
pattern: RegExp,
124+
): RegExpExecArray | undefined {
125+
const stripped = stripHeredocBodies(s)
126+
const mask = buildQuoteMask(stripped)
127+
const re = pattern.global
128+
? pattern
129+
: new RegExp(pattern.source, pattern.flags + 'g')
130+
re.lastIndex = 0
131+
let match: RegExpExecArray | null
132+
while ((match = re.exec(stripped)) !== null) {
133+
if (!mask[match.index]) {
134+
return match
135+
}
136+
if (match.index === re.lastIndex) {
137+
re.lastIndex += 1
138+
}
139+
}
140+
return undefined
141+
}
142+
143+
/**
144+
* Convenience predicate: true when `pattern` matches `s` at an
145+
* unquoted, non-heredoc position. Wraps matchOutsideQuotes.
146+
*/
147+
export function containsOutsideQuotes(s: string, pattern: RegExp): boolean {
148+
return matchOutsideQuotes(s, pattern) !== undefined
149+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// node --test specs for the shared bash-quote-mask helper.
2+
//
3+
// Run from this dir:
4+
// node --test test/*.test.mts
5+
6+
import test from 'node:test'
7+
import assert from 'node:assert/strict'
8+
9+
import {
10+
buildQuoteMask,
11+
containsOutsideQuotes,
12+
matchOutsideQuotes,
13+
stripHeredocBodies,
14+
} from '../bash-quote-mask.mts'
15+
16+
test('buildQuoteMask: empty string', () => {
17+
assert.deepEqual(buildQuoteMask(''), [])
18+
})
19+
20+
test("buildQuoteMask: plain text is all false", () => {
21+
const mask = buildQuoteMask('git status --short')
22+
assert.ok(mask.every(b => b === false))
23+
})
24+
25+
test('buildQuoteMask: single-quoted region is true', () => {
26+
const s = "echo 'hi'"
27+
const mask = buildQuoteMask(s)
28+
// 'echo ' → 5 false
29+
for (let i = 0; i < 5; i += 1) {
30+
assert.strictEqual(mask[i], false)
31+
}
32+
// "'hi'" → 4 true (open quote, h, i, close quote)
33+
for (let i = 5; i < 9; i += 1) {
34+
assert.strictEqual(mask[i], true)
35+
}
36+
})
37+
38+
test('buildQuoteMask: double-quoted region is true', () => {
39+
const s = 'echo "hi"'
40+
const mask = buildQuoteMask(s)
41+
assert.strictEqual(mask[5], true) // "
42+
assert.strictEqual(mask[6], true) // h
43+
assert.strictEqual(mask[7], true) // i
44+
assert.strictEqual(mask[8], true) // "
45+
})
46+
47+
test('buildQuoteMask: escaped double quote inside double quotes', () => {
48+
const s = 'echo "a\\"b"'
49+
const mask = buildQuoteMask(s)
50+
// 'a' is at index 6, the \\ at 7-8 should be marked, then "b" at 9, " at 10.
51+
assert.strictEqual(mask[5], true) // opening "
52+
assert.strictEqual(mask[6], true) // a
53+
assert.strictEqual(mask[7], true) // backslash (escape)
54+
assert.strictEqual(mask[8], true) // escaped "
55+
assert.strictEqual(mask[9], true) // b
56+
assert.strictEqual(mask[10], true) // closing "
57+
})
58+
59+
test('buildQuoteMask: single quotes do not honor backslash', () => {
60+
// POSIX single quotes: backslash is literal. The runtime string is
61+
// `echo 'a\b'` (10 chars): e c h o ␠ ' a \ b '
62+
const s = "echo 'a\\b'"
63+
const mask = buildQuoteMask(s)
64+
assert.strictEqual(s.length, 10)
65+
// Opening quote through closing quote (indices 5..9) are all masked.
66+
for (let i = 5; i < 10; i += 1) {
67+
assert.strictEqual(mask[i], true, `index ${i} should be masked`)
68+
}
69+
})
70+
71+
test('buildQuoteMask: single quotes nested inside double quotes are text', () => {
72+
// Inside a double-quoted string, ' is just a literal apostrophe.
73+
const s = 'echo "it\'s ok"'
74+
const mask = buildQuoteMask(s)
75+
// Every char from index 5 to end is inside the double-quoted region.
76+
for (let i = 5; i < s.length; i += 1) {
77+
assert.strictEqual(mask[i], true)
78+
}
79+
})
80+
81+
test('stripHeredocBodies: replaces body with spaces, preserves length', () => {
82+
const s = "cat <<EOF\nhello\nworld\nEOF\nrest"
83+
const stripped = stripHeredocBodies(s)
84+
assert.strictEqual(stripped.length, s.length)
85+
// The word `hello` should be blanked out.
86+
assert.ok(!stripped.includes('hello'))
87+
// The opening `<<EOF` and closing `EOF` remain.
88+
assert.ok(stripped.includes('<<EOF'))
89+
// `rest` after the heredoc is untouched.
90+
assert.ok(stripped.endsWith('rest'))
91+
})
92+
93+
test('stripHeredocBodies: handles quoted delimiter', () => {
94+
const s = "cat <<'EOF'\nbody\nEOF"
95+
const stripped = stripHeredocBodies(s)
96+
assert.ok(!stripped.includes('body'))
97+
})
98+
99+
test('stripHeredocBodies: handles tab-stripped form', () => {
100+
const s = "cat <<-EOF\n\tbody\n\tEOF"
101+
const stripped = stripHeredocBodies(s)
102+
assert.ok(!stripped.includes('body'))
103+
})
104+
105+
test('containsOutsideQuotes: matches free text', () => {
106+
assert.ok(containsOutsideQuotes('node --bad-flag foo', /--bad-flag/))
107+
})
108+
109+
test('containsOutsideQuotes: does not match inside single quotes', () => {
110+
assert.ok(
111+
!containsOutsideQuotes(
112+
"echo 'reminder: --bad-flag is gone'",
113+
/--bad-flag/,
114+
),
115+
)
116+
})
117+
118+
test('containsOutsideQuotes: does not match inside double quotes', () => {
119+
assert.ok(
120+
!containsOutsideQuotes(
121+
'echo "reminder: --bad-flag is gone"',
122+
/--bad-flag/,
123+
),
124+
)
125+
})
126+
127+
test('containsOutsideQuotes: does not match inside heredoc body', () => {
128+
assert.ok(
129+
!containsOutsideQuotes(
130+
"git commit -m \"$(cat <<'EOF'\nmention --bad-flag here\nEOF\n)\"",
131+
/--bad-flag/,
132+
),
133+
)
134+
})
135+
136+
test('containsOutsideQuotes: matches when both quoted + unquoted occurrences exist', () => {
137+
assert.ok(
138+
containsOutsideQuotes(
139+
"echo 'tip: --bad-flag' && node --bad-flag foo",
140+
/--bad-flag/,
141+
),
142+
)
143+
})
144+
145+
test('matchOutsideQuotes: returns the unquoted match', () => {
146+
const m = matchOutsideQuotes(
147+
"echo 'noise --x' && node --x foo",
148+
/--x/,
149+
)
150+
assert.ok(m)
151+
// The unquoted occurrence sits at the end, well past the quoted one.
152+
assert.ok(m!.index > 20)
153+
})
154+
155+
test('matchOutsideQuotes: handles non-global regex by cloning', () => {
156+
const m = matchOutsideQuotes('node --x foo', /--x/)
157+
assert.ok(m)
158+
assert.strictEqual(m![0], '--x')
159+
})

.claude/hooks/check-new-deps/index.mts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,14 @@ import type { PackageURL } from '@socketregistry/packageurl-js'
2828
import {
2929
SOCKET_PUBLIC_API_TOKEN,
3030
} from '@socketsecurity/lib/constants/socket'
31+
import { errorMessage } from '@socketsecurity/lib/errors'
3132
import { getDefaultLogger } from '@socketsecurity/lib/logger'
3233
import {
3334
normalizePath,
3435
} from '@socketsecurity/lib/paths/normalize'
3536
import { SocketSdk } from '@socketsecurity/sdk'
3637
import type { MalwareCheckPackage } from '@socketsecurity/sdk'
3738

38-
// Local mirror of build-infra/lib/error-utils#errorMessage. Hook runs
39-
// standalone (no workspace deps beyond @socketsecurity/*) so we can't import
40-
// the shared helper, but the contract is identical.
41-
function errorMessage(error: unknown): string {
42-
return error instanceof Error ? error.message : String(error)
43-
}
44-
4539
const logger = getDefaultLogger()
4640

4741
// Per-request timeout (ms) to avoid blocking the hook on slow responses.

.claude/hooks/check-new-deps/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
"@socketsecurity/sdk": "4.0.1"
1616
},
1717
"devDependencies": {
18-
"@types/node": "24.9.2"
18+
"@types/node": "catalog:"
1919
}
2020
}

0 commit comments

Comments
 (0)