bun — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited bun (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This project uses Bun as its runtime. Prefer Bun-native APIs over Node.js equivalents.
The primary way to run external commands. Cross-platform, auto-escapes interpolated values, throws on non-zero exit by default.
import { $ } from "bun"// Simple command — prints to stdout by default
await $`git clone --depth 1 ${url} ${dest}`
// Capture output as string (.text() auto-quiets)
const sha = await $`git rev-parse HEAD`.cwd(dir).text()
// sha === "<commit-hash>\n"
// Capture output as trimmed string
const sha = (await $`git rev-parse HEAD`.cwd(dir).text()).trim()
// Capture as JSON
const result = await $`echo '{"ok": true}'`.json()
// Read output line-by-line
for await (const line of $`git log --oneline -5`.cwd(dir).lines()) {
console.log(line)
}
// Capture stdout and stderr as Buffers
const { stdout, stderr } = await $`git status`.cwd(dir).quiet()Non-zero exit codes throw ShellError by default:
try {
await $`git clone ${url} ${dest}`
} catch (err) {
console.error(`Exit code: ${err.exitCode}`)
console.error(err.stderr.toString())
}Use .nothrow() to handle exit codes manually:
const { exitCode, stdout, stderr } = await $`git diff --quiet`.cwd(dir).nothrow().quiet()
if (exitCode !== 0) {
// There are unstaged changes
}// Per-command
await $`git status`.cwd("/path/to/repo")
await $`echo $MY_VAR`.env({ ...process.env, MY_VAR: "value" })
// Global defaults
$.cwd("/default/dir")
$.env({ ...process.env, GIT_TERMINAL_PROMPT: "0" })// Pipe between commands
const count = await $`git log --oneline | wc -l`.cwd(dir).text()
// Redirect to file
await $`git diff > ${Bun.file("changes.patch")}`
// Redirect stderr to stdout
const output = await $`git clone ${url} 2>&1`.text()All interpolated values are auto-escaped — no shell injection:
const userInput = "foo; rm -rf /"
await $`echo ${userInput}` // SAFE: treated as single argumentTo pass raw (unescaped) strings:
await $`echo ${{ raw: "$(date)" }}` // Executes command substitutionimport { $ } from "bun"
import type { Result } from "./types"
export async function clone(
url: string,
dest: string,
opts?: { depth?: number; branch?: string }
): Promise<Result<void, GitError>> {
try {
const args = ["git", "clone"]
if (opts?.depth) args.push("--depth", String(opts.depth))
if (opts?.branch) args.push("--branch", opts.branch)
args.push(url, dest)
await $`${args}`.quiet()
return { ok: true, value: undefined }
} catch (err) {
return {
ok: false,
error: new GitError(err.stderr?.toString() ?? err.message),
}
}
}
export async function revParse(dir: string): Promise<Result<string, GitError>> {
try {
const sha = (await $`git rev-parse HEAD`.cwd(dir).text()).trim()
return { ok: true, value: sha }
} catch (err) {
return { ok: false, error: new GitError(err.message) }
}
}Lower-level than $. Use when you need fine-grained control over stdin/stdout streams, IPC, or synchronous execution.
// Async
const proc = Bun.spawn(["git", "clone", url, dest], {
cwd: "/tmp",
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
stdout: "pipe", // capture stdout as ReadableStream
stderr: "pipe", // capture stderr as ReadableStream
})
const exitCode = await proc.exited
const stdout = await proc.stdout.text()
// Sync (blocking — good for CLI tools)
const result = Bun.spawnSync(["git", "rev-parse", "HEAD"], { cwd: dir })
if (result.success) {
console.log(result.stdout.toString().trim())
}| Use case | API |
|---|---|
| Running git commands, shell scripts | Bun.$ |
| Piping between commands | Bun.$ |
| Simple command + capture output | Bun.$ |
| Streaming stdin/stdout | Bun.spawn |
| IPC with child process | Bun.spawn |
| Sync execution (CLI tools) | Bun.spawnSync |
| Invoking agent CLIs for semantic scan | Bun.spawn (need stdout stream) |
Faster than Node's fs module. Returns a BunFile (lazy, doesn't read until consumed).
// Read file as string
const content = await Bun.file("config.toml").text()
// Read as JSON
const data = await Bun.file("installed.json").json()
// Read as ArrayBuffer
const buf = await Bun.file("binary.dat").arrayBuffer()
// Check if file exists
const exists = await Bun.file("config.toml").exists()
// Get file size
const size = Bun.file("config.toml").size // bytes
// Write string to file
await Bun.write("config.toml", tomlString)
// Write from another BunFile (copy)
await Bun.write("dest.txt", Bun.file("src.txt"))
// Write JSON
await Bun.write("data.json", JSON.stringify(data, null, 2))| Node.js | Bun |
|---|---|
fs.readFileSync(path, "utf-8") | await Bun.file(path).text() |
fs.writeFileSync(path, data) | await Bun.write(path, data) |
fs.existsSync(path) | await Bun.file(path).exists() |
fs.statSync(path).size | Bun.file(path).size |
Node's fs still works in Bun — use it when you need fs.mkdirSync, fs.readdirSync, symlink operations, etc. that Bun.file/Bun.write don't cover.
Jest-compatible test runner. Import from "bun:test".
import { describe, test, expect, beforeAll, afterAll, mock, spyOn } from "bun:test"See references/testing.md for the full test API reference.
import { describe, test, expect, beforeAll, afterAll } from "bun:test"
describe("scanner", () => {
let tmpDir: string
beforeAll(async () => {
tmpDir = await createFixtureRepo()
})
afterAll(async () => {
await fs.rm(tmpDir, { recursive: true })
})
test("finds root SKILL.md", async () => {
const skills = await scanForSkills(tmpDir)
expect(skills).toHaveLength(1)
expect(skills[0].name).toBe("test-skill")
})
test("validates frontmatter", async () => {
const skills = await scanForSkills(tmpDir)
expect(skills[0].valid).toBe(true)
expect(skills[0].description).toMatch(/test/)
})
test.todo("handles deep scan with confirmation")
})bun test # Run all tests
bun test scanner # Filter by filename
bun test -t "validates" # Filter by test name
bun test --watch # Watch mode
bun test --timeout 10000 # 10s per-test timeout
bun test --bail # Stop on first failureBun uses the standard package.json workspaces field. Same as npm/yarn.
// Root package.json
{
"name": "skilltap-monorepo",
"private": true,
"workspaces": ["packages/*"]
}Reference workspace packages with workspace:*:
// packages/cli/package.json
{
"name": "skilltap",
"dependencies": {
"@skilltap/core": "workspace:*"
},
"devDependencies": {
"@skilltap/test-utils": "workspace:*"
}
}Import workspace packages normally:
import { installSkill } from "@skilltap/core"Bun-specific configuration. Lives at project root.
[install]
# Use exact versions by default
exact = true
[test]
# Preload files before tests
preload = ["./packages/test-utils/src/setup.ts"]
# Default timeout
timeout = 10000Compile to a standalone binary with no runtime dependency:
bun build --compile packages/cli/src/index.ts --outfile skilltapThe binary includes the Bun runtime — runs on machines without Bun installed.
bun build --compile --target=bun-linux-x64 packages/cli/src/index.ts --outfile skilltap-linux
bun build --compile --target=bun-darwin-arm64 packages/cli/src/index.ts --outfile skilltap-macosconst hash = Bun.hash("some string") // fast non-crypto hash
const sha = new Bun.CryptoHasher("sha256")
.update("content")
.digest("hex")const tmpDir = `${import.meta.dir}/../.tmp/${crypto.randomUUID()}`
await fs.mkdir(tmpDir, { recursive: true })
// ... use tmpDir ...
await fs.rm(tmpDir, { recursive: true })import { join, resolve, dirname, basename } from "node:path"
// Node path utilities work perfectly in BunBun.env.HOME // same as process.env.HOME
Bun.env.XDG_CONFIG_HOME // standard config path~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.