Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1e06f1e
chore(cascade): sync allowScripts with allowBuilds
jdalton May 28, 2026
93023f9
fix(hooks): migrate .claude/hooks/<name>/ → .claude/hooks/fleet/<name>/
jdalton May 28, 2026
dc7306c
feat(scripts): seed cross-org publish allowlist + cascade groundwork
jdalton May 29, 2026
fa9ab7e
fix(hooks): remove doubled-directory orphans from earlier migration
jdalton May 29, 2026
89456d1
chore(hooks): cascade fleet hook updates from wheelhouse template
jdalton May 29, 2026
925997a
chore(claude): cascade Claude config + skills from wheelhouse
jdalton May 29, 2026
1757d06
docs(claude.md/fleet): cascade fleet doc updates from wheelhouse
jdalton May 29, 2026
e7fb9f6
chore(config): cascade oxlint + oxfmt config from wheelhouse
jdalton May 29, 2026
4db5857
chore(scripts): cascade fleet script refreshes from wheelhouse
jdalton May 29, 2026
67ea681
chore(claude-md): cascade CLAUDE.md fleet block + .gitattributes refresh
jdalton May 29, 2026
03240e1
chore(deps): bump pnpm 11.3 → 11.4 + lib catalog 5.28 → 6.0.5
jdalton May 29, 2026
ad06339
chore: post-rebase fleet cascade + stale-import fixups
jdalton May 29, 2026
d1106c5
fix(deps): add missing root deps for hook subtree tsgo resolution
jdalton May 29, 2026
c6b1793
fix(build): migrate pnpm external-tools entry to platforms schema
jdalton Jun 1, 2026
cbdd200
chore(claude): rehome skills under .claude/skills/fleet/
jdalton Jun 1, 2026
8e9deba
chore(claude): rehome commands under .claude/commands/fleet/
jdalton Jun 1, 2026
99ba890
chore(claude): rehome agents under .claude/agents/fleet/
jdalton Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
name: security-reviewer
description: Reviews findings from AgentShield + zizmor against the project's CLAUDE.md security rules and grades the result A-F. Spawned by the scanning-security skill after the static scans run.
model: claude-opus-4-8
tools: Read, Grep, Glob, Bash(git:*), Bash(rg:*), Bash(grep:*), Bash(find:*), Bash(ls:*), Bash(pnpm exec agentshield:*), Bash(zizmor:*), Bash(command -v:*), Bash(cat:*), Bash(head:*), Bash(tail:*)
---

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ If they don't, proceed with SFW free mode.
Then run:

```bash
node .claude/hooks/setup-security-tools/index.mts
node .claude/hooks/fleet/setup-security-tools/index.mts
```

After the script completes, add the SFW shim directory to PATH:
Expand All @@ -42,4 +42,4 @@ export PATH="$HOME/.socket/_wheelhouse/shims:$PATH"
- SFW binary is cached via dlx at `~/.socket/_dlx/`
- SFW shims are shared across repos at `~/.socket/_wheelhouse/shims/`
- `.env.local` must NEVER be committed
- `/update` will check for new versions of these tools via `node .claude/hooks/setup-security-tools/update.mts`
- `/update` will check for new versions of these tools via `node .claude/hooks/fleet/setup-security-tools/update.mts`
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# acorn-wasm — shared parser for fleet hooks
# acorn — shared wasm parser for fleet hooks

Vendored from
[`@ultrathink/acorn-monorepo`](https://github.com/SocketDev/ultrathink/tree/main/packages/acorn)'s
Expand All @@ -13,7 +13,7 @@ The three vendored files come straight from the ultrathink prod build:

- `acorn.wasm` — compiled Rust acorn parser, ~3.3 MB.
- `acorn-bindgen.cjs` — wasm-bindgen JS glue.
- `acorn-wasm-sync.mts` — sync ESM loader (no top-level await,
- `acorn-sync.mts` — sync ESM loader (no top-level await,
`WebAssembly.Instance` constructed at module import).

The artifact is rebuilt in ultrathink with `pnpm run
Expand All @@ -39,12 +39,12 @@ Last refreshed: 2026-05-20 (ultrathink build dated 2026-05-20).

## Public surface

`template/.claude/hooks/_shared/acorn/index.mts` is the canonical
`template/.claude/hooks/fleet/_shared/acorn/index.mts` is the canonical
import path for fleet hooks. It re-exports a narrow `tryParse` /
`walkSimple` / `findBareCallsTo` surface — see the module's JSDoc for
the parse-failure tolerance + visitor patterns hook authors rely on.

Don't import `acorn-wasm-sync.mts` directly from hooks; the `index.mts`
Don't import `acorn-sync.mts` directly from hooks; the `index.mts`
wrapper provides the failure-handling + visitor adapters every hook
needs.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* @file Shared acorn-wasm wrapper for fleet hooks. Vendored from
* socket-lib/vendor/acorn-wasm pending the `@ultrathink/acorn` npm publish;
* once that lands, fleet hooks switch to the published package and this
* directory can be retired. Surface kept narrow: `parse(source, opts)` for
* raw AST + `simple(source, visitors, opts)` for visitor-based walks.
* Higher-level shape detectors (`findCallsTo`, `findBareCallsTo`) cover the
* common "lint a specific identifier call" pattern that hooks need.
* socket-lib/vendor/acorn pending the `@ultrathink/acorn` npm publish; once
* that lands, fleet hooks switch to the published package and this directory
* can be retired. Surface kept narrow: `parse(source, opts)` for raw AST +
* `simple(source, visitors, opts)` for visitor-based walks. Higher-level
* shape detectors (`findCallsTo`, `findBareCallsTo`) cover the common "lint a
* specific identifier call" pattern that hooks need.
*/

import { parse as wasmParse, simple as wasmSimple } from './acorn-wasm-sync.mts'
import { parse as wasmParse, simple as wasmSimple } from './acorn-sync.mts'

export interface AcornNode {
type: string
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
/**
* @file Single source of truth for fleet-repo membership, shared by the
* hooks that need to know "is this one of ours?":
* @file Single source of truth for fleet-repo membership, shared by the hooks
* that need to know "is this one of ours?":
*
* - `cross-repo-guard` — blocks `../<fleet-repo>/…` sibling-path imports.
* - `no-non-fleet-push-guard` — blocks `git push` to a repo not in the
* fleet (a non-fleet repo never has the fleet hook chain installed, so
* the guard has to live agent-side and know the roster itself).
*
* This is the BROAD membership set, intentionally wider than the cascade
* roster in `cascading-fleet/lib/fleet-repos.json` (which lists only
* template-cascade targets and omits e.g. `ultrathink`). Membership here
* answers "may fleet tooling act on this repo at all", not "does the
* wheelhouse cascade into it". Keep the two distinct: a repo can be a
* fleet member (pushable, importable) without being a cascade target.
* - `no-non-fleet-push-guard` — blocks `git push` to a repo not in the fleet (a
* non-fleet repo never has the fleet hook chain installed, so the guard has
* to live agent-side and know the roster itself). This is the BROAD
* membership set, intentionally wider than the cascade roster in
* `cascading-fleet/lib/fleet-repos.json` (which lists only template-cascade
* targets and omits e.g. `ultrathink`). Membership here answers "may fleet
* tooling act on this repo at all", not "does the wheelhouse cascade into
* it". Keep the two distinct: a repo can be a fleet member (pushable,
* importable) without being a cascade target.
*/

// All under the SocketDev org. Names match the GitHub repo slug
Expand Down Expand Up @@ -40,24 +39,24 @@ const FLEET_REPO_SET: ReadonlySet<string> = new Set(FLEET_REPO_NAMES)

/**
* True when `slug` (a bare repo name like `socket-cli`) is a fleet member.
* Case-insensitive — GitHub slugs are case-insensitive and remotes can be
* typed in any case.
* Case-insensitive — GitHub slugs are case-insensitive and remotes can be typed
* in any case.
*/
export function isFleetRepo(slug: string): boolean {
return FLEET_REPO_SET.has(slug.toLowerCase())
}

/**
* Extract the bare repo slug from a git remote URL, or `undefined` when the
* URL isn't a recognizable GitHub remote. Handles the three forms git emits:
* Extract the bare repo slug from a git remote URL, or `undefined` when the URL
* isn't a recognizable GitHub remote. Handles the three forms git emits:
*
* git@github.com:SocketDev/socket-cli.git (SSH scp-like)
* ssh://git@github.com/SocketDev/socket-cli.git (SSH URL)
* https://github.com/SocketDev/socket-cli.git (HTTPS, optional .git)
* Git@github.com:SocketDev/socket-cli.git (SSH scp-like)
* ssh://git@github.com/SocketDev/socket-cli.git (SSH URL)
* https://github.com/SocketDev/socket-cli.git (HTTPS, optional .git)
*
* Returns the slug only (`socket-cli`), lowercased. The owner is dropped on
* purpose: membership is keyed on the repo name, and a fork under a
* different owner is still not a fleet push target.
* purpose: membership is keyed on the repo name, and a fork under a different
* owner is still not a fleet push target.
*/
export function slugFromRemoteUrl(url: string): string | undefined {
const trimmed = url.trim()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
/**
* @file Shared heuristic for "which dirty paths in this checkout were authored
* by ANOTHER agent, not this session". Two responsibilities the parallel-agent
* hooks (and overeager-staging-guard) share:
* by ANOTHER agent, not this session". Two responsibilities the
* parallel-agent hooks (and overeager-staging-guard) share:
*
* 1. `readTouchedPaths(transcriptPath)` — the set of absolute paths THIS
* session modified: Edit / Write `file_path` targets plus `git add|mv|rm
* <path>` arguments parsed out of Bash commands. Lifted here from
* 1. `readTouchedPaths(transcriptPath)` — the set of absolute paths THIS session
* modified: Edit / Write `file_path` targets plus `git add|mv|rm <path>`
* arguments parsed out of Bash commands. Lifted here from
* overeager-staging-guard so the three consumers share one implementation
* instead of drifting copies.
* 2. `listForeignDirtyPaths(repoDir, touched, opts)` — dirty paths
* (`git status --porcelain`) that this session did NOT touch and whose
* mtime is recent (so stale pre-session dirt doesn't false-fire). These are
* the likely fingerprints of a concurrent Claude session sharing the
* `.git/` — the failure mode where `git add -A` / `git stash` / `git
* reset --hard` would sweep up or destroy another agent's work.
*
* Fail-open contract (matches the rest of `_shared/`): every helper returns a
* safe default on any parse / I/O error rather than throwing. A hook that
* crashes wedges every Claude Code call; one that returns "nothing foreign"
* simply falls through to the hook's default decision.
* 2. `listForeignDirtyPaths(repoDir, touched, opts)` — dirty paths (`git status
* --porcelain`) that this session did NOT touch and whose mtime is recent
* (so stale pre-session dirt doesn't false-fire). These are the likely
* fingerprints of a concurrent Claude session sharing the `.git/` — the
* failure mode where `git add -A` / `git stash` / `git reset --hard` would
* sweep up or destroy another agent's work. Fail-open contract (matches
* the rest of `_shared/`): every helper returns a safe default on any
* parse / I/O error rather than throwing. A hook that crashes wedges every
* Claude Code call; one that returns "nothing foreign" simply falls
* through to the hook's default decision.
*/

import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
Expand All @@ -45,9 +44,13 @@ const UNTRACKED_BY_DEFAULT_PREFIXES = [
const DEFAULT_MAX_AGE_MS = 30 * 60 * 1000

export interface ForeignPathsOptions {
/** Max age (ms) of a dirty path's mtime to count as foreign. */
/**
* Max age (ms) of a dirty path's mtime to count as foreign.
*/
readonly maxAgeMs?: number | undefined
/** Injectable clock for tests. Defaults to `Date.now()`. */
/**
* Injectable clock for tests. Defaults to `Date.now()`.
*/
readonly now?: number | undefined
}

Expand All @@ -62,10 +65,10 @@ export function isUntrackedByDefault(p: string): boolean {

/**
* Parse `git add|mv|rm <path>` arguments out of a Bash command line and add the
* resolved absolute paths to `touched`. Broad forms (`git add .` / `-A`) are NOT
* surgical adds and are skipped — they don't establish authorship of a specific
* file. Tolerates leading `NAME=val` env assignments and `&&` / `;` / `|`
* chains.
* resolved absolute paths to `touched`. Broad forms (`git add .` / `-A`) are
* NOT surgical adds and are skipped — they don't establish authorship of a
* specific file. Tolerates leading `NAME=val` env assignments and `&&` / `;` /
* `|` chains.
*/
export function addTouchedFromBash(
command: string,
Expand Down Expand Up @@ -173,7 +176,7 @@ export interface DirtyEntry {

/**
* Parse `git status --porcelain` output, dropping untracked-by-default trees.
* Rename entries (`R old -> new`) resolve to the new path.
* Rename entries (`R old -> new`) resolve to the new path.
*/
export function parsePorcelain(out: string): DirtyEntry[] {
const entries: DirtyEntry[] = []
Expand All @@ -196,11 +199,11 @@ export function parsePorcelain(out: string): DirtyEntry[] {
/**
* Dirty paths this session did NOT author and that changed recently — the
* fingerprint of a concurrent agent on the same `.git/`. A path qualifies when:
* - it's dirty (modified / deleted / untracked, minus vendored trees), AND
* - its resolved absolute path is not in `touched`, AND
* - its on-disk mtime is within `maxAgeMs` of `now`.
* Deleted paths (no mtime) are included only if their status is `D`/`R` — a
* delete by another agent is still foreign. Returns repo-relative paths.
* - it's dirty (modified / deleted / untracked, minus vendored trees), AND -
* its resolved absolute path is not in `touched`, AND - its on-disk mtime is
* within `maxAgeMs` of `now`. Deleted paths (no mtime) are included only if
* their status is `D`/`R` — a delete by another agent is still foreign. Returns
* repo-relative paths.
*/
export function listForeignDirtyPaths(
repoDir: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,12 @@ export function detectBroadGitAdd(command: string): string | undefined {
}
for (let k = 0, { length } = c.args; k < length; k += 1) {
const arg = c.args[k]!
if (arg === '--all' || arg === '-A' || arg === '--update' || arg === '-u') {
if (
arg === '--all' ||
arg === '-A' ||
arg === '--update' ||
arg === '-u'
) {
return `git add ${arg}`
}
if (arg === '.') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,31 +49,51 @@ test('parseCommands: comments dropped', () => {
})

test('findInvocation: matches plain git push', () => {
assert.ok(findInvocation('git push origin main', { binary: 'git', subcommand: 'push' }))
assert.ok(
findInvocation('git push origin main', {
binary: 'git',
subcommand: 'push',
}),
)
})

test('findInvocation: matches git -C <dir> push (subcommand after option value)', () => {
assert.ok(findInvocation('git -C /x push', { binary: 'git', subcommand: 'push' }))
assert.ok(
findInvocation('git -C /x push', { binary: 'git', subcommand: 'push' }),
)
})

test('findInvocation: matches git -c k=v push', () => {
assert.ok(findInvocation('git -c foo=bar push', { binary: 'git', subcommand: 'push' }))
assert.ok(
findInvocation('git -c foo=bar push', {
binary: 'git',
subcommand: 'push',
}),
)
})

test('findInvocation: matches push reached via && chain', () => {
assert.ok(
findInvocation('cd /x/depot && git push', { binary: 'git', subcommand: 'push' }),
findInvocation('cd /x/depot && git push', {
binary: 'git',
subcommand: 'push',
}),
)
})

test('findInvocation: matches push in a pipe chain', () => {
assert.ok(
findInvocation('ls | grep x && git push', { binary: 'git', subcommand: 'push' }),
findInvocation('ls | grep x && git push', {
binary: 'git',
subcommand: 'push',
}),
)
})

test('findInvocation: a different subcommand does not match', () => {
assert.ok(!findInvocation('git status', { binary: 'git', subcommand: 'push' }))
assert.ok(
!findInvocation('git status', { binary: 'git', subcommand: 'push' }),
)
})

test('findInvocation: quoted "git push" in a commit message is NOT a push', () => {
Expand Down Expand Up @@ -122,7 +142,9 @@ test('commandsFor: binary-in-a-path is NOT the binary', () => {
})

test('invocationHasFlag: exact flag', () => {
assert.ok(invocationHasFlag('codex --write prompt', 'codex', ['--write', '-w']))
assert.ok(
invocationHasFlag('codex --write prompt', 'codex', ['--write', '-w']),
)
assert.ok(invocationHasFlag('codex -w prompt', 'codex', ['--write', '-w']))
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ export function isTokenKey(key: string): boolean {
* inspection).
*
* Kept short to minimize false positives. A "PASSWORD" mention in a
* commit-message body would otherwise trip every commit, so token-guard
* narrows matches to assignment / flag-value positions rather than any
* occurrence in arbitrary text.
* commit-message body would otherwise trip every commit, so token-guard narrows
* matches to assignment / flag-value positions rather than any occurrence in
* arbitrary text.
*/
export const SENSITIVE_NAME_FRAGMENTS: readonly string[] = [
'TOKEN',
Expand Down
46 changes: 46 additions & 0 deletions .claude/hooks/fleet/alpha-sort-reminder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# alpha-sort-reminder

PreToolUse Edit/Write hook that nudges (never blocks) when a non-code file edit
introduces a sibling block that looks unsorted. oxlint only sees JS/TS, so the
`socket/sort-*` lint rules can't reach JSON / YAML / markdown / bash. This hook
covers those surfaces per [`docs/claude.md/fleet/sorting.md`](../../../../docs/claude.md/fleet/sorting.md).

## What it flags

| Surface | Detects | Key shape |
| -------------------------------------------------- | ------------------------------------------------------------------------- | ----------- |
| JSON / JSONC (`.json`, `.jsonc`, `.oxlintrc.json`) | runs of object keys at one indent, out of ASCII order | `"name": …` |
| YAML (`.yml`, `.yaml`) | runs of mapping keys at one indent (`env:` / `with:` / matrix) | `name:` |
| Markdown (`.md`, `.markdown`) | runs of `-`/`*` bullets out of order; bullets ending in `…`/`...` | `- text` |
| Bash (`.sh`, `.bash`) | runs of all-caps `NAME=…` assignments out of order (cache-key var blocks) | `NAME=…` |

Detection is conservative: **3+** adjacent siblings at the same indent, ASCII
byte order only. False quiet beats false nag: a missed block is a review catch,
while a wrong nag trains the agent to ignore the hook.

## Trigger

Fires on `Edit` / `Write` tool calls. Reads `tool_input.file_path` +
`content`/`new_string` from the PreToolUse payload on stdin. Always exits 0; the
reminder is informational on stderr.

## Bypass

No phrase; the hook never blocks. Silence it entirely with the env var
`SOCKET_ALPHA_SORT_REMINDER_DISABLED=1`. For a genuinely order-bearing block,
just leave it unsorted and state the reason inline (the hook is advisory; review
honors the stated reason).

## Why

John-David has asked for alphanumeric sorting across every file type repeatedly
(2026-04-17 → 2026-05-29: JSON config keys, README consumer lists, workflow YAML
matrix + bash cache-key vars, "no ellipsis"). Code surfaces got lint rules; the
non-code surfaces had no enforcement. This hook closes that gap at edit time.

## Companion files

- `index.mts` — the hook; `findUnsortedBlocks(filePath, content)` is the pure,
exported detector.
- `test/index.test.mts` — node:test specs.
- `package.json` — workspace declaration so `taze` can see the hook's deps.
Loading