subprocess-safety
Skill517 starsChanged 3 days agoMITView on GitHub
Guidelines for safe subprocess calls in opencode-swarm. Load before adding, modifying, or reviewing any file that calls spawn, spawnSync, bunSpawn, or child_process. Covers the six required properties, Windows portability, _internals DI seam pattern, and verification grep.
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 4 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 43
Every subprocess call MUST follow this pattern:
- Very specific: line 144
When validating integer IDs from API responses (PR numbers, issue numbers, run IDs), use Number.isInteger(), not Number.isFinite().
- Distinctive: line 169
Use dependency injection instead:
- Distinctive: line 190
After changing any file with subprocess calls, run:
- Distinctive: line 196
Every match MUST have all of:
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Subprocess Safety
Read, in order:
AGENTS.md(Invariant 3: subprocesses)docs/engineering-invariants.md(subsection 3).agents/skills/writing-tests/SKILL.mdif tests are touched.agents/skills/mock-to-internals-migration/SKILL.mdif converting mock.module to _internals
Codex-specific execution notes:
- This skill consolidates AGENTS.md Invariant 3 into an actionable checklist.
- The canonical spawn shape and six required properties are non-negotiable per AGENTS.md.
- The CI quality job enforces these via
bun run check:invariants(Check 1: subprocess timeout). - Violations are advisory in CI but blocking in code review.
When to use this skill
- You are adding, modifying, or reviewing a subprocess call (
bunSpawn,spawn,spawnSync,child_process.execFile, etc.) - You are writing or updating tests that exercise subprocess-dependent code
- A PR review flags a subprocess call missing timeout, cwd, or cleanup
Scope
This skill applies to all files that spawn child processes:
src/utils/git*.tssrc/hooks/*.tssrc/tools/*.tssrc/services/*.tssrc/plugins/*.tssrc/index.ts(init-path subprocesses)- Any test file (
tests/**) that stubs or exercises subprocess code
Canonical spawn shape
Every subprocess call MUST follow this pattern:
const PER_CALL_TIMEOUT_MS = 10_000; // module-level constant (choose an appropriate value)
const proc = bunSpawn(['git', '-C', dir, 'rev-parse', '--show-toplevel'], {
stdin: 'ignore',
cwd: dir,
timeout: PER_CALL_TIMEOUT_MS,
// stdout/stderr: piped, bounded, or ignored
});
try {
const result = await proc;
// process result
} finally {
proc.kill(); // best-effort cleanup
}Six required properties
| Property | Required | Rationale |
| Array-form args | Yes | No shell-string commands (injection risk, quoting hell) |
cwd or git -C | Yes | Never rely on inherited process.cwd() |
stdin: 'ignore' | Yes | A never-closed stdin pipe under Bun/Windows can block child exit (v7.3.3) |
timeout: <ms> | Yes | No subprocess is "always fast" on every platform |
| stdout/stderr bounded | Yes | Never leave piped stream unattended on long-running child |
proc.kill() in finally | Yes | Outer withTimeout lets awaiter proceed but doesn't abort child |
execFile callback vs execFileSync distinction
child_process.execFile (callback form) and child_process.execFileSync have different
default stdio behavior:
| API | Default stdin | Risk |
execFileSync | 'inherit' | Child inherits parent stdin — v7.3.3 vector on Windows/Bun if stdin is never closed |
execFile (callback) | 'pipe' | Child gets an internal pipe — lower risk but still not ideal for defense-in-depth |
Key differences from the canonical spawn pattern:
proc.kill()infinally(line 69): Applicable to callback-formexecFile. The function returns aChildProcessreference (matching the canonical spawn pattern per Node.js docs). The child reference enableskill()before that point for timeout safety, and failing to callproc.kill()infinallycan leave orphaned children when combined with an outerwithTimeout. Thetimeoutoption triggers internalSIGTERM, but is not a substitute for explicit kill infinally— always kill the child infinally.stdin: 'ignore'(line 66): Technically default-safe for callbackexecFile(stdin is piped, not inherited). However, always addstdio: ['ignore', 'pipe', 'pipe']for defense-in-depth and consistency withexecFileSynccalls. Note: Bun's TypeScript definitions do not includestdioinExecFileOptions— useexecOpts as anywhen passing stdio to callback-formexecFile.execFileSyncshould always usestdio: ['ignore', 'pipe', 'pipe']to prevent the stdin-inheritance hang on Windows/Bun (v7.3.3).
Windows-specific notes
.cmdextensions: npm/bun binaries on Windows are.cmdwrappers. Resolve the executable path explicitly usingwhich/whereor the project's cross-platform helper. Do NOT enableshell: trueor shell-mediated execution to work around PATH resolution.- PATH differences:
cmd.exeand PowerShell resolve PATH differently. Test on Windows, not just macOS/Linux. child_process.spawn('bin', ...)does not behave identically to running undercmd.exe. Use array-form args and explicitcwd.fs.renameSynccannot overwrite existing directories on Windows. Use a remove-then-rename pattern orfs.renamewith error handling.
gh CLI Subprocess Patterns
The gh CLI is a common subprocess in this repo (scripts/release-notes-fragments.mjs, CI workflows). It follows the same six required properties as all subprocesses, plus several gh-specific patterns.
gh api --paginate requires --slurp
Bug pattern (PR #1762 F-002): gh api --paginate without --slurp produces concatenated JSON arrays on stdout. JSON.parse() can only parse the first array — subsequent arrays cause a parse error or are silently lost.
Correct pattern:
const raw = execFileSync('gh', ['api', '--paginate', '--slurp', 'repos/.../pulls', ...], {
encoding: 'utf8',
timeout: 30_000,
maxBuffer: 16 * 1024 * 1024,
stdio: ['ignore', 'pipe', 'pipe'], // required for execFileSync (AGENTS.md §3)
});
// --slurp wraps paginated results as [[page1], [page2], ...]
const pages = JSON.parse(raw);
const allItems = pages.flat(); // flatten to single arrayWithout --slurp: stdout is [item1, item2][item3, item4] — invalid JSON after the first array. This is a silent data loss bug that only manifests when results span multiple pages (>30 items by default).
stdin: 'ignore' for gh calls
gh subprocess calls must include stdin: 'ignore' (or stdio: ['ignore', 'pipe', 'pipe'] for execFileSync). This is the same invariant as all subprocesses (AGENTS.md §3). For example, scripts/release-notes-fragments.mjs defines ghJson() and ghText() helpers using execFileSync — these must include stdio: ['ignore', 'pipe', 'pipe'] per the six required properties. A PR review (pre-merge) identified this gap.
Number.isInteger() for API response validation
When validating integer IDs from API responses (PR numbers, issue numbers, run IDs), use Number.isInteger(), not Number.isFinite(). Number.isFinite() accepts floats like 1.5, which are never valid IDs.
// Correct
function isValidPrNumber(n) {
return Number.isInteger(n) && n > 0;
}
// Wrong — accepts 1.5, NaN, Infinity
function isValidPrNumber(n) {
return Number.isFinite(n) && n > 0;
}Note: This is a stricter pattern. Some existing code uses
Number.isFinite()afterparseInt()— while technically safe for parsed integers,Number.isInteger()is the correct guard for all ID validation going forward.
maxBuffer for large API responses
gh api can return large payloads. Set maxBuffer: 16 * 1024 * 1024 (16 MiB) to prevent silent truncation. This is especially important for --paginate calls that aggregate multiple pages.
Note:
maxBufferis specific to Node.jschild_process.execFile/execFileSync. For Bun'sbunSpawn, use the equivalent output bounding option.
Testing pattern: _internals DI seam, NOT mock.module
mock.module(...) leaks across test files in Bun's shared test-runner process.
Use dependency injection instead:
// --- source file (e.g. src/utils/gitignore-warning.ts) ---
import { bunSpawn } from './bun-compat';
export const _internals: { bunSpawn: typeof bunSpawn } = { bunSpawn };
// In production code, call _internals.bunSpawn(...) instead of bunSpawn(...)
// --- test file ---
import { _internals } from '../../src/utils/gitignore-warning';
const real = _internals.bunSpawn;
beforeEach(() => { _internals.bunSpawn = stub; });
afterEach(() => { _internals.bunSpawn = real; });For the full migration protocol, load the mock-to-internals-migration skill.
Verification grep
After changing any file with subprocess calls, run:
grep -n "bunSpawn\|spawn(\|spawnSync(" src/<changed>/*.tsEvery match MUST have all of:
timeoutset to a concrete millisecond valuestdin: 'ignore'(unless intentionally interactive; note: callback-formexecFileusesstdio: ['ignore', 'pipe', 'pipe']instead)cwdorgit -C <directory>for explicit working directoryproc.kill()in afinallyblock or equivalent cleanup path (exception: callback-formexecFilemanages cleanup internally viatimeoutoption)
Historical failures
- v7.0.3 (#704): repo-graph Desktop hang -- unbounded filesystem scan on plugin init. No timeout, no kill path. Result: "no agents in TUI/GUI" with no error message.
- v7.3.3 (#732): Git-hygiene startup regression --
ensureSwarmGitExcludedcalled git without timeout, stdin, or kill. Result: same silent failure on Windows.
Both caused OpenCode to silently drop the plugin manifest. Users saw no agents and no error. Every subprocess call is a potential repeat of these failures unless all six properties are enforced.
Other versions1 project uses a version of this.
Word for word · 1
- zaxbysauce/opencode-swarm★ 517100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository66 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- ci-fix-monitorSkill
- codebase-review-swarmSkill
- commit-prSkill
- contributingSkill
- engineering-conventionsSkill
- fork-pr-operationsSkill
- guardrail-patternsSkill
- issue-tracerSkill
- mock-to-internals-migrationSkill
- pr-review-fixSkill
- qa-sweepSkill
- research-firstSkill
- running-testsSkill
- swarm-ci-monitorSkill
- swarm-implementSkill
- swarm-pr-feedbackSkill
- swarm-pr-reviewSkill
- swarm-pr-subscribeSkill
- swarmSkill
- tech-debt-ci-reviewSkill
- test-file-splitSkill
- unswarmSkill
- writing-testsSkill
- brainstormSkill
- clarifySkill
- clarify-specSkill
- codebase-review-swarmSkill
- commit-prSkill
- consultSkill
- councilSkill
- critic-gateSkill
- deep-diveSkill
- deep-researchSkill
- design-docsSkill
- discoverSkill
- durable-session-stateSkill
- editing-skillsSkill
- engineering-conventionsSkill
- executeSkill
- fork-pr-operationsSkill
- graphifySkill
- issue-ingestSkill
- issue-tracerSkill
- orchestrating-subagentsSkill
- phase-wrapSkill
- pre-phase-briefingSkill
- qa-sweepSkill
- research-firstSkill
- rust-crate-ciSkill
- specifySkill
- swarm-ci-monitorSkill
- swarm-implementSkill
- swarm-planSkill
- swarm-pr-feedbackSkill
- swarm-pr-reviewSkill
- swarm-pr-subscribeSkill
- swarm-resumeSkill
- The first 60 shown.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

