process
Scripts run inside the agent process and have direct access to the global process.
Axon extends it with two methods for shell execution:
interface AgentProcess extends NodeJS.Process {
// Blocking — run a command and await the full result. Never throws.
run(
command: string,
opts?: { cwd?: string; env?: Record<string, string>; input?: string }
): Promise<{ ok: boolean; exitCode: number; stdout: string; stderr: string; err?: string }>
// Detached — returns a LiveProcHandle immediately. Tracked in AIR <processes>.
spawn(
command: string,
opts?: { cwd?: string; env?: Record<string, string> }
): LiveProcHandle
// Blocked — the capsule manages its own lifecycle
exit: never
}
Everything else is unchanged: process.env, process.cwd(), process.platform,
process.pid, process.argv.
Both run and spawn are gated by the capsule policy. If policy.proc is false or the
command matches no allow rule, the call is blocked before execution and a
capsule:proc:denied event is emitted. See policy.
run
Runs a command, waits for it, returns the result. Never throws — failure is
result.ok === false.
const result = await process.run("bun test")
if (!result.ok) {
throw new Error(`Tests failed:\n${result.stderr}`)
}
process.run(
command: string,
opts?: {
cwd?: string // defaults to process.cwd()
env?: Record<string, string> // merged over process.env, never replaces it
input?: string // written to stdin before start
}
): Promise<{
ok: boolean
exitCode: number
stdout: string
stderr: string
err?: string // set when ok === false
}>
Use it when you need the complete output before continuing — a test suite, a build, a CLI returning structured output:
const build = await process.run("bun run build", { cwd: workspace.root })
if (!build.ok) throw new Error(`Build failed (exit ${build.exitCode}):\n${build.stderr}`)
const result = await process.run("jq '.name'", { input: JSON.stringify({ name: "axon" }) })
console.log(result.stdout.trim()) // "axon"
spawn
Starts a command and returns a LiveProcHandle immediately. The process runs in the
background, tracked by the capsule and visible to the agent in the AIR <processes>
context on every cognitive tick.
const proc = process.spawn("bun dev")
await proc.waitFor("ready on port")
State
proc.procId // string — unique ID tracked by the capsule
proc.command // string — original command
proc.pid // number | undefined
proc.cwd // string | undefined
proc.status // "running" | "exited"
proc.exitCode // number | undefined — set after exit
proc.startedAt // number — unix ms
proc.endedAt // number | undefined — set after exit
Interaction
proc.kill() // terminate
proc.stdin(data) // write a string to stdin
Output
All output methods return content buffered since spawn.
proc.stdout() // full stdout
proc.stdout("stderr") // stderr only
proc.stdout(["stdout", "stderr"]) // both, interleaved
proc.tail(n) // last n lines
proc.extract(regex) // every match from stdout
Waiting
const { exitCode, ok, stdout } = await proc.exited // resolves on exit
const { line, stdout } = await proc.waitFor("Listening on") // resolves on a match
const { line } = await proc.waitFor(/ready/, { timeoutMs: 10_000 })
Streaming
for await (const line of proc.watch()) { ... } // every line
for await (const line of proc.watch(/compiled in/)) { ... } // filtered
const off = proc.on(/error/, line => console.error(line)) // callback
off() // unsubscribe
Querying the buffer
const snapshot = proc.query({
search: "FAIL", // substring match
regex: /FAIL\s+\d+/, // regex match — use one or the other
context: 3, // lines either side of each match
lines: 100, // cap on lines considered
include: ["stdout", "stderr"],
caseSensitive: false,
})
snapshot.lines // every line in the buffer
snapshot.matches // matched entries, each with .text, .before, .after
snapshot.totalLines
snapshot.matchedLines
snapshot.raw // the whole buffer as one string
Patterns
Wait for a server, use it, kill it:
const server = process.spawn("bun run server.ts")
await server.waitFor("Listening on", { timeoutMs: 15_000 })
const result = await axon.request("check the server health")
server.kill()
Inspect a failure after the fact:
const proc = process.spawn("bun test --reporter=verbose")
const { ok } = await proc.exited
if (!ok) {
const failures = proc.query({ search: "FAIL", context: 5 })
for (const match of failures.matches) {
console.log(match.text)
console.log(match.after.join("\n"))
}
}
env
The capsule inherits the agent's environment. process.env holds every key from the
agent's .env and from axon.config.ts, available immediately with no imports.
const token = process.env.GITHUB_TOKEN
Keys come from three sources, in precedence order:
.env— agent-local secrets. Never committed, never published.env.pass— keys forwarded from the host machine's environment.env.set— static values in the config.
The capsule does not inherit the host environment. Only keys named in env.pass are
forwarded, which is what stops a host secret leaking into an agent by accident.
// axon.config.ts
export default defineAgent({
env: {
needs: ["GITHUB_TOKEN", "LINEAR_API_KEY"], // documented as required
pass: ["HOME", "PATH"], // forwarded from host
set: { NODE_ENV: "production" }, // static
},
})
needs is documentation, not enforcement — it tells axon dev what to warn about and
lists requirements when someone installs a published agent.
In cloud deployments the project's .env values are injected as environment variables
at runtime and appear in process.env exactly as local keys do, so agent source is
identical local and deployed. They are not included in the published source artifact and
never shown in deployment logs.