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:

  1. .env — agent-local secrets. Never committed, never published.
  2. env.pass — keys forwarded from the host machine's environment.
  3. 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.