Skip to content

Commit 9043d31

Browse files
committed
chore(wheelhouse): cascade template@5473ac3c
1 parent f2c483f commit 9043d31

16 files changed

Lines changed: 543 additions & 333 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# token-spend-guard
2+
3+
PreToolUse hook that reminds (non-fatal `exit 2`) when a **known-mechanical** Bash command runs on a **premium model or high reasoning effort**. Enforces the "Token spend: match model + effort to the job" rule.
4+
5+
## What it catches
6+
7+
A command whose shape is unambiguously mechanical — wheelhouse cascade (`pnpm run sync`, `chore(wheelhouse): cascade` commit), whole-tree lint autofix (`oxlint --fix .` / `fix --all`), or format sweep (`oxfmt --write .`) — while:
8+
9+
- the model (read from the transcript's most-recent assistant `model` field) is an Opus, **or**
10+
- `$CLAUDE_EFFORT` is `high` / `xhigh` / `max`.
11+
12+
Each dimension is flagged and bypassed independently. `low`/`medium` effort and Sonnet/Haiku never trigger — they're already the cheap/fast tier.
13+
14+
## Why
15+
16+
Mechanical work is dumb-bit propagation; a cheap/fast model at low/medium effort handles it fine. Spending premium model + high-effort tokens on cascades and autofix sweeps is wasted money. The premium tier is for design, ambiguous debugging, and security review. The trigger set is deliberately narrow so the guard never nags during real work — a false trigger would train reflex-bypassing, which defeats the rule.
17+
18+
## Bypass
19+
20+
- `Allow model bypass` (keep the premium model for this task) — also accepts `Allow model-spend bypass`.
21+
- `Allow effort bypass` (keep high effort for this task).
22+
- `SOCKET_TOKEN_SPEND_GUARD_DISABLED=1` (disable entirely).
23+
24+
## Test
25+
26+
```sh
27+
pnpm test
28+
```
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env node
2+
// Claude Code PreToolUse hook — token-spend-guard.
3+
//
4+
// Reminds (exit 2, non-fatal nudge) when a KNOWN-MECHANICAL command runs on a
5+
// premium model or high reasoning effort. Mechanical work — cascades, lint-
6+
// autofix sweeps, rename/path migrations — is dumb-bit propagation that a
7+
// cheap/fast model at low/medium effort handles fine; spending `opus` +
8+
// `high`/`xhigh`/`max` tokens on it is wasted money. Design work (architecture,
9+
// ambiguous debugging, security review) is what the premium tier is for.
10+
//
11+
// Two signals, both observable to a PreToolUse hook:
12+
// - effort: the `$CLAUDE_EFFORT` env var (low|medium|high|xhigh|max), set by
13+
// the harness for tool-use-context hooks.
14+
// - model: read from the transcript's most-recent assistant event `model`
15+
// field (the payload itself carries no model outside SessionStart).
16+
//
17+
// Only fires on a command whose shape is unambiguously mechanical, so it never
18+
// nags during real work. Reminder, not a hard block — but it sets exit 2 so the
19+
// agent sees it and either drops the model/effort or types a bypass.
20+
//
21+
// Bypass: "Allow model bypass" (keep the premium model) or "Allow effort
22+
// bypass" (keep high effort) in a recent user turn, or
23+
// SOCKET_TOKEN_SPEND_GUARD_DISABLED=1.
24+
25+
import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'
26+
import process from 'node:process'
27+
28+
import { withBashGuard } from '../_shared/payload.mts'
29+
import { bypassPhrasePresent, readLines } from '../_shared/transcript.mts'
30+
31+
const logger = getDefaultLogger()
32+
33+
const MODEL_BYPASS = ['Allow model bypass', 'Allow model-spend bypass'] as const
34+
const EFFORT_BYPASS = ['Allow effort bypass'] as const
35+
36+
// Effort levels that count as "premium" — the tiers worth conserving on
37+
// mechanical work. low/medium are already cheap, so they never trigger.
38+
const PREMIUM_EFFORT = new Set(['high', 'xhigh', 'max'])
39+
40+
// A model id is "premium" when it's an Opus. Sonnet/Haiku are the cheap/fast
41+
// tier the guard nudges toward. Matches both alias and full-id shapes
42+
// (`opus`, `claude-opus-4-8`, `claude-opus-4-8[1m]`).
43+
function isPremiumModel(model: string): boolean {
44+
return /\bopus\b/i.test(model) || /claude-opus/i.test(model)
45+
}
46+
47+
// Command shapes that are unambiguously mechanical. Kept deliberately narrow:
48+
// a false trigger on real work would train the agent to reflex-bypass, which
49+
// defeats the rule. Each entry is a substring/RE checked against the command.
50+
const MECHANICAL_RE = [
51+
// Wheelhouse cascade sync + its commit.
52+
/\bpnpm\s+run\s+sync\b/,
53+
/chore\(wheelhouse\):\s*cascade\b/,
54+
// Mass autofix / format sweeps (the whole-tree variants, not a single file).
55+
/\b(?:pnpm\s+(?:run|exec)\s+)?(?:oxlint|eslint)\b[^\n]*--fix\b[^\n]*(?:\s\.|--all)\b/,
56+
/\b(?:pnpm\s+run\s+)?fix\b\s+--all\b/,
57+
/\boxfmt\b[^\n]*--write\b[^\n]*\s\.(?:\s|$)/,
58+
] as const
59+
60+
function isMechanical(command: string): boolean {
61+
return MECHANICAL_RE.some(re => re.test(command))
62+
}
63+
64+
// Read the model from the most-recent assistant event in the transcript.
65+
// Returns '' when unreadable — the guard then can't judge the model and only
66+
// considers effort.
67+
function readCurrentModel(transcriptPath: string | undefined): string {
68+
const lines = readLines(transcriptPath)
69+
for (let i = lines.length - 1; i >= 0; i -= 1) {
70+
const line = lines[i]
71+
if (!line || !line.includes('"model"')) {
72+
continue
73+
}
74+
try {
75+
const evt = JSON.parse(line) as { model?: unknown; type?: unknown }
76+
if (typeof evt.model === 'string' && evt.model) {
77+
return evt.model
78+
}
79+
} catch {
80+
// Skip malformed lines.
81+
}
82+
}
83+
return ''
84+
}
85+
86+
await withBashGuard((command, payload) => {
87+
if (process.env['SOCKET_TOKEN_SPEND_GUARD_DISABLED']) {
88+
return
89+
}
90+
if (!isMechanical(command)) {
91+
return
92+
}
93+
94+
const effort = String(process.env['CLAUDE_EFFORT'] ?? '').toLowerCase()
95+
const model = readCurrentModel(payload.transcript_path)
96+
97+
const effortIsPremium = PREMIUM_EFFORT.has(effort)
98+
const modelIsPremium = !!model && isPremiumModel(model)
99+
100+
// Each dimension is independently bypassable, so only flag the dimensions
101+
// that are both premium AND not bypassed for this turn.
102+
const flagModel =
103+
modelIsPremium &&
104+
!bypassPhrasePresent(payload.transcript_path, MODEL_BYPASS)
105+
const flagEffort =
106+
effortIsPremium &&
107+
!bypassPhrasePresent(payload.transcript_path, EFFORT_BYPASS)
108+
109+
if (!flagModel && !flagEffort) {
110+
return
111+
}
112+
113+
const lines = [
114+
'[token-spend-guard] Mechanical command on a premium setting.',
115+
'',
116+
]
117+
if (flagModel) {
118+
lines.push(
119+
` model : ${model} — premium. Mechanical work runs fine on a`,
120+
' cheap/fast model. Switch: /model sonnet (or haiku).',
121+
' Keep it for this task: type "Allow model bypass".',
122+
)
123+
}
124+
if (flagEffort) {
125+
lines.push(
126+
` effort : ${effort} — premium. Drop it: /effort low (or medium).`,
127+
' Keep it for this task: type "Allow effort bypass".',
128+
)
129+
}
130+
lines.push(
131+
'',
132+
' Mechanical = cascades, lint-autofix sweeps, rename/path migrations.',
133+
' Reserve premium model + high effort for design, hard debugging,',
134+
' security review. Disable entirely: SOCKET_TOKEN_SPEND_GUARD_DISABLED=1.',
135+
'',
136+
)
137+
logger.error(lines.join('\n') + '\n')
138+
process.exitCode = 2
139+
})
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "hook-token-spend-guard",
3+
"private": true,
4+
"type": "module",
5+
"main": "./index.mts",
6+
"exports": {
7+
".": "./index.mts"
8+
},
9+
"scripts": {
10+
"test": "node --test test/*.test.mts"
11+
},
12+
"devDependencies": {
13+
"@types/node": "catalog:"
14+
}
15+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { test } from 'node:test'
2+
import assert from 'node:assert/strict'
3+
import { spawnSync } from '@socketsecurity/lib-stable/process/spawn/child'
4+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
5+
import os from 'node:os'
6+
import path from 'node:path'
7+
import { fileURLToPath } from 'node:url'
8+
9+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
10+
const HOOK_PATH = path.join(__dirname, '..', 'index.mts')
11+
12+
// Build a transcript whose most-recent assistant event uses `model`, plus an
13+
// optional user line (for bypass-phrase tests).
14+
function makeTranscript(model: string, userText?: string): string {
15+
const dir = mkdtempSync(path.join(os.tmpdir(), 'tokenspend-'))
16+
const p = path.join(dir, 'session.jsonl')
17+
const lines = []
18+
if (userText) {
19+
lines.push(JSON.stringify({ role: 'user', content: userText }))
20+
}
21+
lines.push(JSON.stringify({ type: 'assistant', model, content: [] }))
22+
writeFileSync(p, lines.join('\n'))
23+
return p
24+
}
25+
26+
function runHook(
27+
command: string,
28+
transcriptPath: string,
29+
effort: string,
30+
extraEnv: Record<string, string> = {},
31+
): { stderr: string; exitCode: number } {
32+
const result = spawnSync('node', [HOOK_PATH], {
33+
input: JSON.stringify({
34+
tool_name: 'Bash',
35+
tool_input: { command },
36+
transcript_path: transcriptPath,
37+
}),
38+
env: { ...process.env, CLAUDE_EFFORT: effort, ...extraEnv },
39+
})
40+
return { stderr: String(result.stderr), exitCode: result.status ?? -1 }
41+
}
42+
43+
const CASCADE = 'pnpm run sync --target . --fix'
44+
45+
test('REMINDS on mechanical command + premium model (opus)', () => {
46+
const t = makeTranscript('claude-opus-4-8')
47+
const { stderr, exitCode } = runHook(CASCADE, t, 'low')
48+
assert.equal(exitCode, 2)
49+
assert.match(stderr, /token-spend-guard/)
50+
assert.match(stderr, /premium/)
51+
assert.match(stderr, /opus/)
52+
})
53+
54+
test('REMINDS on mechanical command + premium effort (high)', () => {
55+
const t = makeTranscript('claude-sonnet-4-6')
56+
const { stderr, exitCode } = runHook(CASCADE, t, 'high')
57+
assert.equal(exitCode, 2)
58+
assert.match(stderr, /effort/)
59+
})
60+
61+
test('ALLOWS mechanical command on cheap model + low effort', () => {
62+
const t = makeTranscript('claude-sonnet-4-6')
63+
const { exitCode } = runHook(CASCADE, t, 'low')
64+
assert.equal(exitCode, 0)
65+
})
66+
67+
test('ALLOWS a non-mechanical command even on premium model + high effort', () => {
68+
const t = makeTranscript('claude-opus-4-8')
69+
const { exitCode } = runHook('git status', t, 'high')
70+
assert.equal(exitCode, 0)
71+
})
72+
73+
test('model bypass silences the model flag (effort low → fully clears)', () => {
74+
const t = makeTranscript('claude-opus-4-8', 'Allow model bypass')
75+
const { exitCode } = runHook(CASCADE, t, 'low')
76+
assert.equal(exitCode, 0)
77+
})
78+
79+
test('effort bypass silences the effort flag (cheap model → fully clears)', () => {
80+
const t = makeTranscript('claude-sonnet-4-6', 'Allow effort bypass')
81+
const { exitCode } = runHook(CASCADE, t, 'max')
82+
assert.equal(exitCode, 0)
83+
})
84+
85+
test('one bypass does NOT silence the other dimension', () => {
86+
// opus + high, only model bypassed → effort still flags.
87+
const t = makeTranscript('claude-opus-4-8', 'Allow model bypass')
88+
const { stderr, exitCode } = runHook(CASCADE, t, 'high')
89+
assert.equal(exitCode, 2)
90+
assert.match(stderr, /effort/)
91+
assert.doesNotMatch(stderr, /Switch: \/model/)
92+
})
93+
94+
test('cascade commit subject triggers the guard', () => {
95+
const t = makeTranscript('claude-opus-4-8')
96+
const { exitCode } = runHook(
97+
'git commit -m "chore(wheelhouse): cascade template@abc123"',
98+
t,
99+
'low',
100+
)
101+
assert.equal(exitCode, 2)
102+
})
103+
104+
test('disabled env var short-circuits', () => {
105+
const t = makeTranscript('claude-opus-4-8')
106+
const { exitCode } = runHook(CASCADE, t, 'high', {
107+
SOCKET_TOKEN_SPEND_GUARD_DISABLED: '1',
108+
})
109+
assert.equal(exitCode, 0)
110+
})
111+
112+
test('IGNORES non-Bash tools', () => {
113+
const result = spawnSync('node', [HOOK_PATH], {
114+
input: JSON.stringify({
115+
tool_name: 'Write',
116+
tool_input: { command: CASCADE },
117+
}),
118+
env: { ...process.env, CLAUDE_EFFORT: 'high' },
119+
})
120+
assert.equal(result.status, 0)
121+
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"compilerOptions": {
3+
"declarationMap": false,
4+
"erasableSyntaxOnly": true,
5+
"module": "nodenext",
6+
"moduleResolution": "nodenext",
7+
"noEmit": true,
8+
"rewriteRelativeImportExtensions": true,
9+
"skipLibCheck": true,
10+
"sourceMap": false,
11+
"strict": true,
12+
"target": "esnext",
13+
"types": ["node"],
14+
"verbatimModuleSyntax": true
15+
}
16+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* @file Unit tests for the no-top-level-await oxlint rule. Spawns the real
3+
* oxlint binary against fixture files in a tmp dir (see lib/rule-tester.mts).
4+
* Skips silently when `oxlint` isn't on PATH so a fresh-laptop checkout
5+
* doesn't false-fail before `pnpm install` materializes the bin link.
6+
*
7+
* Why the rule exists: fleet bundles publish to CJS (rolldown CJS output)
8+
* and CJS does not support module-scope `await`. A regression there either
9+
* fails the bundle outright or silently emits an uninitialized export.
10+
* The valid cases pin the supported escape hatches (await inside an async
11+
* function, an async IIFE, the `socket-hook: allow top-level-await`
12+
* comment) so a future refactor can't quietly drop them.
13+
*/
14+
15+
import { describe, test } from 'node:test'
16+
17+
import rule from '../rules/no-top-level-await.mts'
18+
import { RuleTester } from '../lib/rule-tester.mts'
19+
20+
describe('socket/no-top-level-await', () => {
21+
test('valid + invalid cases', () => {
22+
new RuleTester().run('no-top-level-await', rule, {
23+
valid: [
24+
{
25+
name: 'await inside async function',
26+
code: 'async function f() { await Promise.resolve() }\n',
27+
},
28+
{
29+
name: 'await inside async arrow',
30+
code: 'const f = async () => { await Promise.resolve() }\n',
31+
},
32+
{
33+
name: 'await inside async IIFE',
34+
code: ';(async () => { await Promise.resolve() })()\n',
35+
},
36+
{
37+
name: 'bypass comment opts module out',
38+
code: '// socket-hook: allow top-level-await\nawait Promise.resolve()\n',
39+
},
40+
],
41+
invalid: [
42+
{
43+
name: 'top-level await expression',
44+
code: 'await Promise.resolve()\n',
45+
errors: [{ messageId: 'banned' }],
46+
},
47+
{
48+
name: 'top-level for await',
49+
code: 'for await (const x of [1, 2]) {}\n',
50+
errors: [{ messageId: 'banned' }],
51+
},
52+
],
53+
})
54+
})
55+
})

0 commit comments

Comments
 (0)