Skip to content

Commit dfd39d3

Browse files
committed
chore(wheelhouse): cascade template@360e4aa4
Auto-applied by socket-wheelhouse sync-scaffolding into socket-bin. 9 file(s) touched: - .claude/hooks/fleet/no-non-fleet-push-guard/index.mts - .claude/hooks/fleet/non-fleet-pr-issue-ask-guard/index.mts - .claude/hooks/fleet/paths-mts-inherit-guard/README.md - .claude/hooks/fleet/paths-mts-inherit-guard/index.mts - .claude/hooks/fleet/paths-mts-inherit-guard/test/index.test.mts - .gitattributes - docs/claude.md/fleet/bypass-phrases.md - pnpm-workspace.yaml - scripts/fleet/paths.mts
1 parent 8758908 commit dfd39d3

9 files changed

Lines changed: 118 additions & 28 deletions

File tree

.claude/hooks/fleet/no-non-fleet-push-guard/index.mts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,21 @@ import { bypassPhrasePresent } from '../_shared/transcript.mts'
4242

4343
const logger = getDefaultLogger()
4444

45+
// Bare, session-wide form (kept as a fallback). The scoped form below
46+
// is preferred — it names the exact repo so the authorization can't
47+
// leak to an unrelated non-fleet push later in the session.
4548
const BYPASS_PHRASE = 'Allow non-fleet-push bypass'
49+
const BYPASS_PHRASE_PREFIX = 'Allow non-fleet-push bypass:'
50+
51+
// Build the phrases that authorize a push to `slug`. Accept the full
52+
// `owner/repo` slug and its bare repo basename, so the user can write
53+
// whichever feels natural (e.g. `SocketDev/socket-bin` or `socket-bin`).
54+
// The bare session-wide phrase stays accepted for back-compat.
55+
export function acceptedBypassPhrases(slug: string): string[] {
56+
const basename = slug.includes('/') ? slug.slice(slug.indexOf('/') + 1) : slug
57+
const targets = basename === slug ? [slug] : [slug, basename]
58+
return [BYPASS_PHRASE, ...targets.map(t => `${BYPASS_PHRASE_PREFIX} ${t}`)]
59+
}
4660

4761
export function originSlug(dir: string): string | undefined {
4862
let out: string
@@ -85,7 +99,7 @@ await withBashGuard((command, payload) => {
8599

86100
if (
87101
payload.transcript_path &&
88-
bypassPhrasePresent(payload.transcript_path, BYPASS_PHRASE)
102+
bypassPhrasePresent(payload.transcript_path, acceptedBypassPhrases(slug))
89103
) {
90104
return
91105
}
@@ -107,7 +121,13 @@ await withBashGuard((command, payload) => {
107121
` git -C ${dir} remote get-url origin`,
108122
'',
109123
` If the push is genuinely intended (a personal / non-fleet repo`,
110-
` you own), type "${BYPASS_PHRASE}" in a new message, then retry.`,
124+
` you own), type the scoped phrase for THIS repo in a new message,`,
125+
' then retry:',
126+
` ${BYPASS_PHRASE_PREFIX} ${slug}`,
127+
'',
128+
` The scoped form authorizes ${slug} only — it can’t leak to an`,
129+
' unrelated non-fleet push later. (The bare, session-wide',
130+
` "${BYPASS_PHRASE}" is still accepted as a fallback.)`,
111131
'',
112132
].join('\n'),
113133
)

.claude/hooks/fleet/non-fleet-pr-issue-ask-guard/index.mts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,19 @@ import { bypassPhrasePresent } from '../_shared/transcript.mts'
4747

4848
const logger = getDefaultLogger()
4949

50+
// Bare, session-wide form (kept as a fallback). The scoped form is
51+
// preferred — it names the exact repo so authorization can't leak to an
52+
// unrelated non-fleet publish later in the session.
5053
const BYPASS_PHRASE = 'Allow non-fleet-publish bypass'
54+
const BYPASS_PHRASE_PREFIX = 'Allow non-fleet-publish bypass:'
55+
56+
// Phrases that authorize a publish to `slug`: the bare fallback plus the
57+
// scoped form against the full `owner/repo` slug and its bare basename.
58+
export function acceptedBypassPhrases(slug: string): string[] {
59+
const basename = slug.includes('/') ? slug.slice(slug.indexOf('/') + 1) : slug
60+
const targets = basename === slug ? [slug] : [slug, basename]
61+
return [BYPASS_PHRASE, ...targets.map(t => `${BYPASS_PHRASE_PREFIX} ${t}`)]
62+
}
5163

5264
const GH_DASH_REPO_RE = /--repo[\s=]+("([^"]+)"|'([^']+)'|(\S+))/
5365

@@ -142,7 +154,7 @@ await withBashGuard((command, payload) => {
142154
// Non-fleet target. Check bypass phrase.
143155
if (
144156
payload.transcript_path &&
145-
bypassPhrasePresent(payload.transcript_path, BYPASS_PHRASE)
157+
bypassPhrasePresent(payload.transcript_path, acceptedBypassPhrases(slug))
146158
) {
147159
return
148160
}
@@ -159,9 +171,13 @@ await withBashGuard((command, payload) => {
159171
` submit without explicit per-action user confirmation —`,
160172
` captured plans + "do all N tasks" directives do NOT count.`,
161173
'',
162-
` If you really want to submit: type the canonical phrase`,
163-
` in your next message, then re-run:`,
164-
` ${BYPASS_PHRASE}`,
174+
` If you really want to submit: type the scoped phrase for THIS`,
175+
` repo in your next message, then re-run:`,
176+
` ${BYPASS_PHRASE_PREFIX} ${slug}`,
177+
'',
178+
` The scoped form authorizes ${slug} only — it can’t leak to an`,
179+
` unrelated non-fleet publish later. (The bare, session-wide`,
180+
` "${BYPASS_PHRASE}" is still accepted as a fallback.)`,
165181
'',
166182
' Otherwise: draft locally, share for review, get explicit',
167183
' yes/no before re-attempting.',

.claude/hooks/fleet/paths-mts-inherit-guard/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,19 @@ The fleet rule from CLAUDE.md (1 path, 1 reference):
2020
2121
## Allowed shapes
2222

23-
Repo-root `scripts/paths.mts` — no ancestor exists; nothing to
24-
inherit from. Skipped.
23+
Repo-root paths module — no ancestor exists; nothing to inherit from.
24+
Skipped. The canonical location is `scripts/fleet/paths.mts` after the
25+
`scripts/{fleet,repo}` segmentation; the pre-segmentation `scripts/paths.mts`
26+
is also recognized for repos that haven't moved yet.
2527

2628
Sub-package `packages/foo/scripts/paths.mts`:
2729

2830
```ts
29-
export * from '../../../scripts/paths.mts'
31+
export * from '../../../scripts/fleet/paths.mts'
3032

3133
// Local overrides below — package-specific paths.
3234
import path from 'node:path'
33-
import { REPO_ROOT } from '../../../scripts/paths.mts'
35+
import { REPO_ROOT } from '../../../scripts/fleet/paths.mts'
3436
export const FOO_DIST = path.join(REPO_ROOT, 'packages', 'foo', 'dist')
3537
```
3638

.claude/hooks/fleet/paths-mts-inherit-guard/index.mts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,22 +69,37 @@ const PATHS_MTS_RE = /(?:^|\/)paths\.(?:cts|mts)$/
6969
const EXPORT_STAR_RE =
7070
/^\s*export\s+\*\s+from\s+['"](?:[^'"]+\/paths\.m?ts)['"];?\s*$/m
7171

72+
// Ancestor paths.mts can live at `scripts/paths.{mts,cts}` (per-package
73+
// convention) OR `scripts/fleet/paths.{mts,cts}` (the repo-root canonical
74+
// module after the scripts/{fleet,repo} segmentation moved it under fleet/).
75+
// Probe both, in directory-depth order, so the walk finds the nearest ancestor
76+
// whether or not a given repo has been segmented yet.
77+
const ANCESTOR_REL_CANDIDATES: readonly string[] = [
78+
'scripts/paths.mts',
79+
'scripts/paths.cts',
80+
'scripts/fleet/paths.mts',
81+
'scripts/fleet/paths.cts',
82+
]
83+
7284
/**
73-
* Walk up from `filePath` looking for an ancestor `scripts/paths.mts` or
74-
* `scripts/paths.cts`. Returns the absolute path of the nearest one, or
75-
* `undefined` if there's no ancestor (i.e. this IS the repo- root paths.mts).
85+
* Walk up from `filePath` looking for an ancestor paths module —
86+
* `scripts/paths.{mts,cts}` or `scripts/fleet/paths.{mts,cts}` (the post-
87+
* segmentation repo-root location). Returns the absolute path of the nearest
88+
* one, or `undefined` if there's no ancestor (i.e. this IS the repo-root
89+
* paths.mts).
7690
*
7791
* Stops at the first ancestor found OR at the filesystem root.
7892
*/
7993
export function findAncestorPathsMts(filePath: string): string | undefined {
80-
const fileDir = path.dirname(path.resolve(filePath))
94+
const resolvedSelf = path.resolve(filePath)
95+
const fileDir = path.dirname(resolvedSelf)
8196
// Skip the current file's own dir — we want a STRICT ancestor.
8297
let cur = path.dirname(fileDir)
8398
const root = path.parse(cur).root
8499
while (cur && cur !== root) {
85-
for (const ext of ['mts', 'cts']) {
86-
const candidate = path.join(cur, 'scripts', `paths.${ext}`)
87-
if (existsSync(candidate) && candidate !== path.resolve(filePath)) {
100+
for (let i = 0, { length } = ANCESTOR_REL_CANDIDATES; i < length; i += 1) {
101+
const candidate = path.join(cur, ANCESTOR_REL_CANDIDATES[i]!)
102+
if (existsSync(candidate) && candidate !== resolvedSelf) {
88103
return candidate
89104
}
90105
}

.claude/hooks/fleet/paths-mts-inherit-guard/test/index.test.mts

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,13 @@ let tmpRoot: string
3434

3535
beforeEach(() => {
3636
tmpRoot = mkdtempSync(path.join(os.tmpdir(), 'paths-mts-inherit-guard-'))
37-
// Repo-root scripts/fleet/paths.mts — ancestor exists for sub-packages.
38-
mkdirSync(path.join(tmpRoot, 'scripts'), { recursive: true })
37+
// Repo-root scripts/fleet/paths.mts — the canonical ancestor after the
38+
// scripts/{fleet,repo} segmentation. findAncestorPathsMts must discover this
39+
// (not just the pre-segmentation scripts/paths.mts) so sub-packages stay
40+
// enforced.
41+
mkdirSync(path.join(tmpRoot, 'scripts', 'fleet'), { recursive: true })
3942
writeFileSync(
40-
path.join(tmpRoot, 'scripts', 'paths.mts'),
43+
path.join(tmpRoot, 'scripts', 'fleet', 'paths.mts'),
4144
"export const REPO_ROOT = '/tmp/fake'\n",
4245
)
4346
})
@@ -70,7 +73,7 @@ describe('paths-mts-inherit-guard', () => {
7073
const r = runHook({
7174
tool_name: 'Write',
7275
tool_input: {
73-
file_path: path.join(tmpRoot, 'scripts', 'paths.mts'),
76+
file_path: path.join(tmpRoot, 'scripts', 'fleet', 'paths.mts'),
7477
content: "export const X = 'no inheritance needed at root'\n",
7578
},
7679
})
@@ -99,6 +102,32 @@ describe('paths-mts-inherit-guard', () => {
99102
assert.match(r.stderr, /export \* from/)
100103
})
101104

105+
test('discovers the ancestor at scripts/fleet/paths.mts (post-segmentation)', () => {
106+
// Regression: before findAncestorPathsMts learned the scripts/fleet/
107+
// location, a sub-package whose only ancestor lives at
108+
// scripts/fleet/paths.mts (the segmented repo-root module) was wrongly
109+
// treated as having NO ancestor → silently exempt. The block must fire,
110+
// and the suggested re-export must point at the fleet location.
111+
mkdirSync(path.join(tmpRoot, 'packages', 'seg', 'scripts'), {
112+
recursive: true,
113+
})
114+
const r = runHook({
115+
tool_name: 'Write',
116+
tool_input: {
117+
file_path: path.join(
118+
tmpRoot,
119+
'packages',
120+
'seg',
121+
'scripts',
122+
'paths.mts',
123+
),
124+
content: "export const REDERIVED = 'wrong'\n",
125+
},
126+
})
127+
assert.equal(r.code, 2)
128+
assert.match(r.stderr, /scripts\/fleet\/paths\.mts/)
129+
})
130+
102131
test('allows sub-package paths.mts WITH export *', () => {
103132
mkdirSync(path.join(tmpRoot, 'packages', 'foo', 'scripts'), {
104133
recursive: true,
@@ -114,7 +143,7 @@ describe('paths-mts-inherit-guard', () => {
114143
'paths.mts',
115144
),
116145
content:
117-
"export * from '../../../scripts/paths.mts'\nexport const FOO_DIST = '/x'\n",
146+
"export * from '../../../scripts/fleet/paths.mts'\nexport const FOO_DIST = '/x'\n",
118147
},
119148
})
120149
assert.equal(r.code, 0)
@@ -133,7 +162,7 @@ describe('paths-mts-inherit-guard', () => {
133162
)
134163
writeFileSync(
135164
subPath,
136-
"export * from '../../../scripts/paths.mts'\nexport const OLD = '/x'\n",
165+
"export * from '../../../scripts/fleet/paths.mts'\nexport const OLD = '/x'\n",
137166
)
138167
const r = runHook({
139168
tool_name: 'Edit',
@@ -147,7 +176,11 @@ describe('paths-mts-inherit-guard', () => {
147176
assert.equal(r.code, 0)
148177
})
149178

150-
test('allows paths.cts variant', () => {
179+
test('allows paths.cts variant (and the pre-segmentation export * target)', () => {
180+
// The guard checks the textual export * line; it does NOT resolve the
181+
// target to disk. So a sub-package re-exporting the OLD pre-segmentation
182+
// path string still satisfies inheritance — un-migrated repos aren't
183+
// suddenly blocked. (The on-disk ancestor here is scripts/fleet/paths.mts.)
151184
mkdirSync(path.join(tmpRoot, 'packages', 'cjs', 'scripts'), {
152185
recursive: true,
153186
})

.gitattributes

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,6 @@ scripts/fleet/util/source-allowlist.mts linguist-generated=true
190190
scripts/fleet/validate-bundle-deps.mts linguist-generated=true
191191

192192
# Vendored binary blobs (no diff, no merge, no text-mode).
193-
.claude/hooks/fleet/_shared/acorn/acorn.wasm binary
194-
template/.claude/hooks/fleet/_shared/acorn/acorn.wasm binary
193+
.claude/hooks/_shared/acorn/acorn.wasm binary
194+
template/.claude/hooks/_shared/acorn/acorn.wasm binary
195195
# ─── END fleet-canonical ────────────────────────────────────────

docs/claude.md/fleet/bypass-phrases.md

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ minimumReleaseAgeExclude:
140140
# published: 2026-05-26 | removable: 2026-06-02
141141
- '@oxc-project/types@0.133.0'
142142
- 'shell-quote'
143+
- '@ultrathink/*'
144+
- '@yuku-parser/*'
143145

144146
pmOnFail: error
145147
resolutionMode: highest

scripts/fleet/paths.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* re-audited. Per-package, like package.json: every package that has its own
99
* `scripts/` directory has its own `paths.mts`. A sub-package can inherit
1010
* from a parent's paths.mts by re-exporting: // packages/foo/bar/paths.mts
11-
* export * from '../../../scripts/paths.mts' // Add sub-package-specific
11+
* export * from '../../../scripts/fleet/paths.mts' // Add sub-package-specific
1212
* overrides below the export line. export const FOO_BAR_DIST =
1313
* path.join(REPO_ROOT, 'packages', 'foo', 'bar', 'dist') Consumers resolve
1414
* `paths.mts` the same way Node resolves `package.json` — relative to the

0 commit comments

Comments
 (0)