Skip to content

Commit cc7c75b

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 ab90838 commit cc7c75b

59 files changed

Lines changed: 1980 additions & 465 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
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.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# excuse-detector
2+
3+
Claude Code `Stop` hook that scans the assistant's most recent turn for excuse-shaped phrases and warns to stderr at end-of-turn.
4+
5+
## Why
6+
7+
CLAUDE.md has two rules the assistant routinely tries to wriggle out of:
8+
9+
- **No "pre-existing" excuse** — fix lint/type/test errors you see in your reading window. Don't label them "pre-existing" and walk past.
10+
- **Unrelated issues are critical** — an adjacent bug is exactly the bug nobody is currently looking for. Don't defer.
11+
12+
The phrases that precede those deferrals are predictable: "pre-existing", "not related to my X", "unrelated to the task", "out of scope", "separate concern", "leave it for later", "not my issue". This hook scans the transcript for them.
13+
14+
## What it catches
15+
16+
| Phrase | Why it's flagged |
17+
|---|---|
18+
| `pre-existing` / `preexisting` | Bare rationalization; CLAUDE.md bans the label. |
19+
| `not related to my <X>` | Scoping out a fix. CLAUDE.md says fix it. |
20+
| `unrelated to the task` | Same. |
21+
| `out of scope` | Same. The genuine exception (large refactor) requires asking, not silent deferral. |
22+
| `separate concern` | Same. |
23+
| `leave it for later` | Deferral marker. CLAUDE.md "Completion" bans deferrals. |
24+
| `not my issue` / `not my problem` | Scoping out. |
25+
26+
## Why it doesn't block
27+
28+
Stop hooks fire *after* the assistant has produced its response. Blocking at that point would just truncate the message — the rationalization is already out. The warning surfaces alongside the response so the user reads both, and can push back in the next turn.
29+
30+
The right enforcement is layered:
31+
32+
- **CLAUDE.md rule** documents the policy.
33+
- **This hook** surfaces violations at end-of-turn.
34+
- **The user** demands the fix in the next turn.
35+
36+
## Configuration
37+
38+
`SOCKET_EXCUSE_DETECTOR_DISABLED=1` — turn the hook off entirely. Useful for sessions where the policy genuinely doesn't apply (e.g. running a long-form review that intentionally calls out scope boundaries).
39+
40+
## Test
41+
42+
```sh
43+
pnpm test
44+
```

0 commit comments

Comments
 (0)