Skip to content

Tools

June ships tools so you don't have to write them. Each one is a plain AI SDK tool() underneath, declared in JSX like any other capability: <Bash /> in an <Agent>'s children gives its steps the tool. Approvals, traces and cancellation work the way they do for your own tools. There is no June tool type, registry or protocol.

The tools themselves are exported from june/tools as functions that return an AI SDK tool, for when you hold a ToolSet or build your own capability. Today there is one: bashTool().

<Bash />

import { openai } from "june/sdk/openai";
import { Agent, Bash, Step } from "june";

export default function InspectPackage() {
  return (
    <Agent model={openai("gpt-5-mini")}>
      <Bash />
      <Step
        id="inspect"
        prompt="List the files in this directory, read package.json, and say what the package is."
      />
    </Agent>
  );
}
june run inspect-package.tsx
▸ inspect
? Allow bash {"command":"ls"} (step inspect)? [y/N]

<Bash /> adds a tool named bash to the agent it's in, built by bashTool() with the props you give it. The same tool works as a plain AI SDK tool, which is how you use it without the component:

import { openai } from "june/sdk/openai";
import { Agent, Step } from "june";
import { bashTool } from "june/tools";

const bash = bashTool();

export default function InspectPackage() {
  return (
    <Agent model={openai("gpt-5-mini")} tools={{ bash }}>
      <Step id="inspect" prompt="List the files in this directory." />
    </Agent>
  );
}

Declare it one way or the other, not both: a second bash in one agent is an error.

The model's input is only { command: string }. The command runs with bash -lc, so pipes, redirects and && work as they do in a terminal. The tool returns a structured result, and the model reads it as the tool's output:

{ "exitCode": 0, "stdout": "package.json\n", "stderr": "" }

A non-zero exitCode is a result, not an error: a failing test run or a grep with no match is something the model should see. The tool throws only when the command can't run (the cwd isn't a directory, Bash can't start) or the call is aborted.

Options

import { Agent, Bash } from "june";
import { openai } from "june/sdk/openai";

export const agent = (
  <Agent model={openai("gpt-5-mini")}>
    <Bash
      cwd="/path/to/repo"
      env={{ CI: "1", NODE_OPTIONS: undefined }}
      maxOutputBytes={32 * 1024}
      needsApproval
    />
  </Agent>
);

bashTool() takes the same options as an object.

Option / propDefault
cwdprocess.cwd()where the command runs; it must be an existing directory
envnoneoverlaid on the June process's environment. A value of undefined removes that variable from the command's environment
maxOutputBytes64 KiBthe most bytes kept of each of stdout and stderr, as a positive integer. Output beyond it is read and dropped, and stdoutTruncated or stderrTruncated is true
needsApprovaltruewhether each call pauses for an approval

Output is cut on a UTF-8 character boundary, keeping the beginning. Stdin is closed, so a command that waits for input gets end of file. Each call is a fresh shell: nothing carries over between calls. When the shell exits, anything it started that still holds stdout or stderr open is stopped after a short grace period, so a background process can't hang the call or outlive it.

Cancellation

The AI SDK's abort signal reaches the command. If the run is aborted by Ctrl-C, --timeout, handle.abort() or a canceled chat turn, June kills the command's whole process group, including anything it started, and the call rejects with an AbortError. A call whose signal is already aborted never starts.

Where it runs

<Bash /> is not a sandbox. Read this before you give it to a model.

  • The command runs on the local machine, with the permissions of the June process. It can read, write and delete whatever you can, and reach whatever network you can.
  • It inherits the June process's environment, including API keys and tokens, except for the keys you override or remove in env.
  • The command and its output are tool input and output: they appear in traces and june run --json, and they go to the model on the next call.
  • needsApproval: true is a gate for an operator, not a security boundary. It lets a person look at a command before it runs, but June doesn't read the command, and doesn't claim to know whether one is safe. A command that looks harmless can still do harm, and an approval doesn't make it safe.
  • Use needsApproval: false only when you trust the model and the task, or have constrained the environment outside June, such as in a container, a disposable machine or a restricted account.

If you need to limit what the model can do, give it narrower tools of your own instead of a shell.

Hosts

The CLI doesn't add Bash to a workflow. A workflow has Bash because it declares <Bash /> in its own <Agent>, and june run, june chat and june ask only host it, asking for approval the way they do for any tool.

For a chat, declare it in the workflow a workflow-backed responder renders. modelResponder(), which createChat({ model }) uses, makes one model call and has no tools.

import { openai } from "june/sdk/openai";
import { agentResponder, createChat } from "june/chat";
import { Agent, Bash, Step } from "june";

export default createChat({
  responder: agentResponder({
    id: "shell",
    render: ({ messages, input }) => (
      <Agent model={openai("gpt-5-mini")}>
        <Bash />
        <Step
          id="reply"
          prompt={[...messages, { role: "user", content: input }]}
        />
      </Agent>
    ),
    reply: (result) => String(result.steps.reply),
  }),
});

june ask chat.tsx "Why do the tests fail?" asks for approval on stderr and reads the answer from stdin. With the prompt piped on stdin there is nothing left to answer with, so every call is denied; Bash is not an exception.

Not included

Interactive terminals, stdin, persistent shells, sandboxing, command allowlists and timeouts. Each one is a larger decision than this tool makes for you. Abort the run to stop a command.