Bundle scripts, references, and assets

Write a safe, self-contained script

Give bundled code clear inputs, useful errors, stable output, documented dependencies, and no surprising side effects.

A skill script may run in a repository you’ve never seen, on a machine you don’t control. Make its boundary boring. Boring is safe.

One input, one job

validate-report.mjs takes one explicit file path. It reads that file, checks the required headings and the decision, prints useful errors, and exits non-zero on failure.

It has no surprising side effects. It doesn’t scan the working directory looking for reports. It doesn’t install anything. It doesn’t rewrite the report to fix it. It doesn’t touch the network. It doesn’t read environment variables.

The interface is one line:

node scripts/validate-report.mjs release-report.md

The core of the script

Here’s the shape, stripped down to the heading check:

import { readFileSync } from 'node:fs'

const file = process.argv[2]
if (!file) {
  console.error('usage: validate-report.mjs <report.md>')
  process.exit(2)
}

const report = readFileSync(file, 'utf8')
const required = ['## Decision', '## Evidence', '## Blockers', '## Warnings']
const missing = required.filter((heading) => !report.includes(heading))

if (missing.length) {
  console.error(`Missing section: ${missing.join(', ')}`)
  process.exit(1)
}

console.log('report structure ok')

Standard library only. Errors on stderr, results on stdout, distinct exit codes. That’s most of what “safe” means here. The real script also checks that the decision is one of the three allowed values.

Errors that help

“Missing section: ## Blockers” tells the agent exactly what to fix. “Invalid input” tells it nothing, and it starts guessing.

Send normal output to standard output and errors to standard error. Use different exit codes for different failures. In the example, 2 means bad usage and 1 means the report failed. The agent, or a CI job, can branch on that.

Dependencies

Use the standard library when it’s enough. Here it is. If you truly need a package, document it beside the command and explain what the host should do when it’s missing. A helper that quietly runs npm install on someone else’s machine is not a helper.

Test it, then attack it

Run the script against five inputs: a valid report, a missing file, a report with one heading removed, a report with Decision: MAYBE, and a filename with spaces in it.

The last one catches a common bug. If you built the command with string concatenation, my report.md becomes two arguments. Reading process.argv[2] directly is fine. Shelling out with the path inside a template string is not.

Then read your own script as an attacker. Could a crafted path execute shell syntax? Could a 2 GB file exhaust memory? Could anything the script does overwrite user data?

The script is part of the skill’s authority. Keep it narrower than the prose, never broader.

Lesson completed