Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .claude/hooks/logger-guard/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ const LOGGER_LEAK_RE =

const COMMENT_LINE_RE = /^\s*(\*|\/\/|#)/
const JSDOC_TAG_RE = /@(example|param|returns?|see|link)\b/
const SOCKET_HOOK_MARKER_RE = /#\s*socket-hook:\s*allow(?:\s+([\w-]+))?/
// Accept `#`, `//`, or `/*` comment prefixes — same as the git pre-
// commit/pre-push scanners. This hook is invoked on TS/JS edits where
// `// socket-hook: allow logger` is the only natural spelling.
const SOCKET_HOOK_MARKER_RE =
/(?:#|\/\/|\/\*)\s*socket-hook:\s*allow(?:\s+([\w-]+))?/

function isMarkerSuppressed(line: string): boolean {
const m = line.match(SOCKET_HOOK_MARKER_RE)
Expand Down Expand Up @@ -216,7 +220,7 @@ function emitBlock(filePath: string, hits: Hit[]): void {
out.push(` …and ${hits.length - 3} more.`)
}
out.push(
' Opt-out for one line (rare): append `// # socket-hook: allow logger`.',
' Opt-out for one line (rare): append `// socket-hook: allow logger`.',
)
out.push('')
process.stderr.write(out.join('\n'))
Expand Down
22 changes: 22 additions & 0 deletions .claude/hooks/logger-guard/test/logger-guard.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,28 @@ test('respects bare # socket-hook: allow marker', async () => {
assert.equal(code, 0)
})

test('respects // socket-hook: allow logger marker (slash-slash prefix)', async () => {
const { code } = await runHook({
tool_name: 'Edit',
tool_input: {
file_path: 'src/foo.ts',
new_string: 'process.stderr.write(buf) // socket-hook: allow logger',
},
})
assert.equal(code, 0)
})

test('respects /* socket-hook: allow logger */ marker (block-comment prefix)', async () => {
const { code } = await runHook({
tool_name: 'Edit',
tool_input: {
file_path: 'src/foo.ts',
new_string: 'console.error("a") /* socket-hook: allow logger */',
},
})
assert.equal(code, 0)
})

test('does not flag JSDoc examples', async () => {
const { code } = await runHook({
tool_name: 'Write',
Expand Down
49 changes: 33 additions & 16 deletions .claude/skills/reviewing-code/run.mts
Original file line number Diff line number Diff line change
Expand Up @@ -384,8 +384,9 @@ Options:
-h, --help Show this help`)
}

async function git(args: readonly string[]): Promise<string> {
async function git(args: readonly string[], cwd?: string): Promise<string> {
const result = await spawn('git', args as string[], {
cwd,
stdio: 'pipe',
stdioString: true,
})
Expand Down Expand Up @@ -453,6 +454,7 @@ async function runBackend(
promptText: string,
tempDir: string,
passLabel: string,
cwd: string,
): Promise<{ ok: boolean; output: string; logPath: string }> {
const desc = BACKENDS[backend]
const promptFile = path.join(tempDir, `${passLabel}.prompt.txt`)
Expand All @@ -464,6 +466,7 @@ async function runBackend(
let stdout = ''
try {
const child = spawn(desc.bin, argv as string[], {
cwd,
stdio: 'pipe',
stdioString: true,
})
Expand Down Expand Up @@ -573,18 +576,20 @@ async function main(): Promise<void> {
logger.error('Must be run inside a git repository.')
process.exit(1)
}
process.chdir(repoRoot)

const branchRaw = await git(['branch', '--show-current'])
const branchRaw = await git(['branch', '--show-current'], repoRoot)
const branch =
branchRaw.length > 0
? branchRaw
: `detached-${(await git(['rev-parse', '--short', 'HEAD']))}`
const baseRef = await resolveBaseRef(args.baseRef)
const mergeBase = await git(['merge-base', baseRef, 'HEAD'])
: `detached-${(await git(['rev-parse', '--short', 'HEAD'], repoRoot))}`
const baseRef = await resolveBaseRef(args.baseRef, repoRoot)
const mergeBase = await git(['merge-base', baseRef, 'HEAD'], repoRoot)
const range = `${mergeBase}..HEAD`
const commitList = await git(['log', '--oneline', '--no-decorate', range])
const diffStat = await git(['diff', '--stat', range])
const commitList = await git(
['log', '--oneline', '--no-decorate', range],
repoRoot,
)
const diffStat = await git(['diff', '--stat', range], repoRoot)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relative reports drift from repo root

Medium Severity

Relative --output paths now resolve from the caller’s current directory, while backend agents run from repoRoot. Running the skill from a subdirectory writes the report elsewhere than the path agents are prompted to read and update.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 628bf25. Configure here.


const outputPath =
args.outputPath ??
Expand Down Expand Up @@ -631,7 +636,13 @@ async function main(): Promise<void> {
}
logger.info(`${passLabel}: running on ${backend}`)
const promptText = ROLES[role].buildPrompt(ctx)
const result = await runBackend(backend, promptText, tempDir, passLabel)
const result = await runBackend(
backend,
promptText,
tempDir,
passLabel,
repoRoot,
)
if (!result.ok) {
logger.error(`${passLabel}: failed; see ${result.logPath}`)
await appendSkipNote(
Expand Down Expand Up @@ -674,17 +685,23 @@ async function main(): Promise<void> {
}
}

async function resolveBaseRef(provided: string | undefined): Promise<string> {
async function resolveBaseRef(
provided: string | undefined,
cwd: string,
): Promise<string> {
if (provided) {
return provided
}
try {
const headRef = await git([
'symbolic-ref',
'--quiet',
'--short',
'refs/remotes/origin/HEAD',
])
const headRef = await git(
[
'symbolic-ref',
'--quiet',
'--short',
'refs/remotes/origin/HEAD',
],
cwd,
)
if (headRef.length > 0) {
return headRef
}
Expand Down
16 changes: 13 additions & 3 deletions .claude/skills/scanning-quality/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ allowed-tools: Task, Read, Grep, Glob, AskUserQuestion, Bash(pnpm run check:*),

Perform comprehensive quality analysis across the codebase using specialized agents. Clean up junk files first, then scan and generate a prioritized report with actionable fixes.

## Modes

- **Default (interactive)** — `AskUserQuestion` is used to confirm cleanup deletions and to pick scan scope.
- **Non-interactive** — `/scanning-quality non-interactive` (or any of the aliases below) skips every `AskUserQuestion` and applies safe defaults: scan scope = all types, cleanup = leave junk files in place (don't delete without confirmation), report-save = yes (`reports/scanning-quality-YYYY-MM-DD.md`). Use this when running headlessly (e.g. `pnpm run fleet-skill scanning-quality`, CI cron, programmatic Claude). The four-flag programmatic-Claude lockdown rule already strips `AskUserQuestion`, so headless runs default to non-interactive automatically — but call it out explicitly so future readers understand the contract.

Detect non-interactive mode via any of: `--non-interactive` argument, `non-interactive` argument, `SCANNING_QUALITY_NONINTERACTIVE=1` env var, or absence of `AskUserQuestion` in the available tool surface.

## Scan Types

1. **critical** - Crashes, security vulnerabilities, resource leaks, data corruption
Expand Down Expand Up @@ -46,7 +53,7 @@ Install zizmor for GitHub Actions security scanning, respecting the soak window

### Phase 4: Repository Cleanup

Find and remove junk files (with user confirmation via AskUserQuestion):
Find junk files (interactive mode confirms each batch via `AskUserQuestion`; non-interactive mode lists what was found in the report and leaves them in place — don't delete files without explicit confirmation, even on a clean dirty-tree):
- SCREAMING_TEXT.md files outside `.claude/` and `docs/`
- Test files in wrong locations
- Temp files (`.tmp`, `.DS_Store`, `*~`, `*.swp`, `*.bak`)
Expand All @@ -62,7 +69,9 @@ Report errors as Critical findings. Warnings are Low findings. (The fleet's stru

### Phase 6: Determine Scan Scope

Ask user which scans to run using AskUserQuestion (multiSelect). Default: all scans.
In **interactive** mode, ask the user which scans to run via `AskUserQuestion` (multiSelect). Default: all scans.

In **non-interactive** mode, run all scan types — no prompt.

### Phase 7: Execute Scans

Expand All @@ -77,7 +86,8 @@ Each agent reports findings as:
- Deduplicate findings across scan types
- Sort by severity: Critical > High > Medium > Low
- Generate markdown report with file:line references, suggested fixes, and coverage metrics
- Offer to save to `reports/scanning-quality-YYYY-MM-DD.md`
- **Interactive**: offer to save to `reports/scanning-quality-YYYY-MM-DD.md` via `AskUserQuestion`.
- **Non-interactive**: save the report unconditionally to `reports/scanning-quality-YYYY-MM-DD.md` (create the directory if missing) so the artifact is visible to the orchestrating runner. If the `Write` tool isn't in the allow list, emit the full markdown to stdout with a leading `=== REPORT MARKDOWN ===` marker so the runner can capture and persist it.

### Phase 9: Summary

Expand Down
38 changes: 33 additions & 5 deletions .git-hooks/_helpers.mts
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,15 @@ const PERSONAL_PATH_PLACEHOLDER_RE =

// Per-line opt-out marker for our pre-commit / pre-push scanners.
//
// Canonical form: # socket-hook: allow
// Targeted form: # socket-hook: allow <rule>
// Canonical form: <comment-prefix> socket-hook: allow
// Targeted form: <comment-prefix> socket-hook: allow <rule>
//
// `<comment-prefix>` is whichever comment style the host file uses —
// `#` for shell / YAML / TOML / Dockerfile, `//` for TS / JS / Rust /
// Go / C-family, or `/*` for the C-block-comment opener. The hook is
// invoked from many file types; pinning to `#` made the marker fail
// silently in `.ts` / `.mts` files (where `// socket-hook: allow` is
// the only sensible spelling) and confused contributors.
//
// The targeted form names a specific rule (`personal-path`, `npx`,
// `aws-key`, etc.) and is recommended for reviewers; the bare `allow`
Expand All @@ -113,8 +120,28 @@ const PERSONAL_PATH_PLACEHOLDER_RE =
// Legacy `# zizmor: ...` markers are still recognized for one cycle so
// existing files don't have to be rewritten in the same change that
// renames the marker.
const SOCKET_HOOK_MARKER_RE = /#\s*socket-hook:\s*allow(?:\s+([\w-]+))?/
const LEGACY_ZIZMOR_MARKER_RE = /#\s*zizmor:\s*[\w-]+/
const SOCKET_HOOK_MARKER_RE =
/(?:#|\/\/|\/\*)\s*socket-hook:\s*allow(?:\s+([\w-]+))?/

// File extensions whose natural comment syntax is `//` (C-family + cousins).
// Anything else falls through to `#` (shell / YAML / TOML / Dockerfile /
// Makefile / Python / Ruby / etc).
const SLASH_COMMENT_EXT_RE =
/\.(m?ts|tsx|cts|m?js|jsx|cjs|rs|go|c|cc|cpp|cxx|h|hpp|java|swift|kt|scala|dart|php|css|scss|less)$/i

/**
* Pick the natural per-line opt-out marker for a host file.
*
* The marker regex above accepts `#`, `//`, and `/*` prefixes — but error
* messages should print the *one* form a contributor would actually paste
* into that file. TS edits get `// socket-hook: allow <rule>`; YAML gets
* `# socket-hook: allow <rule>`. Same rule, different comment lexer.
*/
export const socketHookMarkerFor = (filePath: string, rule: string): string =>
SLASH_COMMENT_EXT_RE.test(filePath)
? `// socket-hook: allow ${rule}`
: `# socket-hook: allow ${rule}`
const LEGACY_ZIZMOR_MARKER_RE = /(?:#|\/\/|\/\*)\s*zizmor:\s*[\w-]+/

function lineIsSuppressed(line: string, rule?: string): boolean {
if (LEGACY_ZIZMOR_MARKER_RE.test(line)) {
Expand Down Expand Up @@ -343,7 +370,8 @@ export const scanNpxDlx = (text: string): LineHit[] => {
// `@socketsecurity/lib/logger`. Direct calls to `process.stderr.write`,
// `process.stdout.write`, `console.log`, `console.error`, `console.warn`,
// `console.info`, `console.debug` are blocked. Doc-context lines are
// exempt; lines carrying `# socket-hook: allow logger` are exempt too.
// exempt; lines carrying `// socket-hook: allow logger` (or `#` in
// non-TS files) are exempt too.

const LOGGER_LEAK_RE =
/\b(process\.std(?:err|out)\.write|console\.(?:log|error|warn|info|debug))\s*\(/
Expand Down
5 changes: 3 additions & 2 deletions .git-hooks/pre-commit.mts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
scanPrivateKeys,
scanSocketApiKeys,
shouldSkipFile,
socketHookMarkerFor,
yellow,
} from './_helpers.mts'

Expand Down Expand Up @@ -195,7 +196,7 @@ const main = (): number => {
out(
"Use 'pnpm exec <package>' or 'pnpm run <script>' instead. For " +
'documentation lines that need the literal `npx` form, append ' +
'the marker `# socket-hook: allow npx`.',
`the marker \`${socketHookMarkerFor(file, 'npx')}\`.`,
)
errors++
}
Expand Down Expand Up @@ -244,7 +245,7 @@ const main = (): number => {
out(
'Use `getDefaultLogger()` from `@socketsecurity/lib/logger`. ' +
'For documentation lines that need the literal call, append ' +
'the marker `# socket-hook: allow logger`.',
`the marker \`${socketHookMarkerFor(file, 'logger')}\`.`,
)
errors++
}
Expand Down
3 changes: 2 additions & 1 deletion .git-hooks/pre-push.mts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
scanPrivateKeys,
scanSocketApiKeys,
shouldSkipFile,
socketHookMarkerFor,
} from './_helpers.mts'

const ZERO_SHA = '0000000000000000000000000000000000000000'
Expand Down Expand Up @@ -315,7 +316,7 @@ const scanFilesInRange = (range: string): number => {
out(
'Use `getDefaultLogger()` from `@socketsecurity/lib/logger`. ' +
'For documentation lines that need the literal call, append ' +
'the marker `# socket-hook: allow logger`.',
`the marker \`${socketHookMarkerFor(file, 'logger')}\`.`,
)
errors++
}
Expand Down
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Dependabot disabled - we manage dependencies manually
# Using open-pull-requests-limit: 0 to disable version updates
# See: https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: yearly
open-pull-requests-limit: 0
cooldown:
default-days: 7
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ Full hook spec in [`.claude/hooks/token-guard/README.md`](.claude/hooks/token-gu
- `/scanning-security` — AgentShield + zizmor audit
- `/scanning-quality` — quality analysis
- Shared subskills in `.claude/skills/_shared/`
- **Handing off to another agent** — see [`docs/references/agent-delegation.md`](docs/references/agent-delegation.md) for when to reach for `codex:codex-rescue`, the `delegate` subagent (OpenCode → Fireworks/Synthetic/Kimi), `Explore`, `Plan`, vs. driving the skill CLIs directly. The CLI-subprocess contract used by skills lives in [`_shared/multi-agent-backends.md`](.claude/skills/_shared/multi-agent-backends.md).

#### Skill scope: fleet vs partial vs unique

Expand Down
39 changes: 39 additions & 0 deletions docs/references/agent-delegation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Agent delegation

When a task fits one of the patterns below, hand it off instead of doing it in the current session. The point is to get a _different model's_ take or to keep heavy work out of the main context — not to avoid effort. Don't delegate trivial tasks: the round-trip overhead isn't worth it for things you can answer in one or two tool calls.

There are two delegation surfaces in this fleet. They look similar but are used differently.

## Surface 1 — CLI subprocess delegation (skills)

Skills that need multi-model output spawn the agent CLIs (`codex`, `claude`, `kimi`, `opencode`) as subprocesses and fold the results into a report. The contract — backend registry, detection policy, fallback order, attribution — lives in [`_shared/multi-agent-backends.md`](../../.claude/skills/_shared/multi-agent-backends.md). The canonical implementation is [`reviewing-code/run.mts`](../../.claude/skills/reviewing-code/run.mts).

Use this surface when _the skill itself_ is the orchestrator (multi-pass review, parallel scans, fleet-wide runs).

## Surface 2 — Subagent delegation (mid-conversation)

When the _current_ Claude session wants to hand off a single task to another model and consume its result inline, use `Agent(subagent_type=…)`. This is in-conversation delegation, not skill orchestration.

| Subagent | When to use |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `codex:codex-rescue` | You want GPT-5.4's take or a heavyweight async investigation. Best for: hard debugging you're stuck on, second implementation pass on a tricky design, deep root-cause work. Persistent runtime — check progress with `/codex:status`, get output with `/codex:result`. Also exposed as `/codex:rescue` for user-driven invocation. |
| `delegate` | You want a Fireworks / Synthetic / Kimi open model via [OpenCode](https://opencode.ai). Best for: cheap bulk work (classification, summarization, drafting many things), specialist routing (e.g. Qwen-Coder for code-heavy tasks), second opinions from a non-GPT/non-Claude model. Caller specifies the model in the prompt (e.g. `fireworks/qwen3-coder-480b`). Fire-and-forget. **Optional** — only available if the dev has set up the `delegate` agent locally. Skill code must not depend on it. |
| `Explore` | Codebase search / "where is X defined" / cross-file lookups. Different model isn't the point — context isolation is. |
| `Plan` | Implementation strategy for a non-trivial task before writing code. |
| `general-purpose` | Open-ended research that doesn't fit the above. |

## Routing heuristics

- **Stuck after one or two failed attempts** → `codex:codex-rescue`. A different family often breaks the deadlock.
- **About to do 20+ similar small operations** → `delegate` with a cheap model. Keep the main context clean.
- **Want a sanity check on a non-trivial design or diff** → `/codex:adversarial-review` (slash command) _or_ `delegate` to a different family, depending on which perspective is more useful.
- **Big codebase question that'll burn context** → `Explore`.
- **Building a multi-pass workflow** → don't use `Agent(...)` ad hoc; write a skill that uses Surface 1.

## When the surfaces overlap

A skill that wants `codex` output should call the CLI (Surface 1) so the result lands in a structured report. A live conversation that wants Codex's opinion on the _current_ problem should use the subagent (Surface 2) so the result flows back into the conversation. Same model, different orchestration.

## Compatibility note

Codex is fleet-wide (the `codex` CLI is a fleet plugin). OpenCode and the `delegate` subagent are **per-developer** — they require local setup outside the repo. Skills that automate work across the fleet must not assume `delegate` exists; humans driving Claude in their own checkout can use it freely.
Loading