Local shell | OpenAI API
For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Primary navigation

Local shell

Enable agents to run commands in a local shell.

The local shell tool is outdated. For new use cases, use the shell tool with GPT-5.1 instead. Learn more.

Local shell is a tool that allows agents to run shell commands locally on a machine you or the user provides. It’s designed to work with Codex CLI and codex-mini-latest. Commands are executed inside your own runtime, so you are fully in control of which commands actually run. The API only returns instructions; it does not execute them on OpenAI infrastructure.

Local shell is available through the Responses API for use with codex-mini-latest. It is not available on other models or via the Chat Completions API.

Running arbitrary shell commands can be dangerous. Always sandbox execution or add strict allowlists or deny lists before forwarding a command to the system shell.


See Codex CLI for reference implementation.

How it works

The local shell tool enables agents to run in a continuous loop with access to a terminal.

The model sends shell commands, which your code executes on a local machine before returning the output to the model. This loop allows the model to complete the build-test-run loop without additional user intervention.

Your code must implement a loop that listens for local_shell_call output items and executes the commands they contain. We strongly recommend sandboxing execution to prevent unexpected commands from running.

Integrating the local shell tool

These are the high-level steps you need to follow to integrate the local shell tool in your application:

  1. Send a request to the model: Include the local_shell tool as part of the available tools.

  2. Receive a response from the model: Check if the response has any local_shell_call items. This tool call contains an action like exec with a command to execute.

  3. Execute the requested action: Run the command in the local environment you control.

  4. Return the action output: After executing the action, return the command output to the model.

  5. Repeat: Send a new request with the updated state as a local_shell_call_output, and repeat this loop until the model stops requesting actions or you decide to stop.

Example workflow

Below is a minimal example showing the request/response loop. Choose a language to see the equivalent workflow for its SDK. For brevity, production-grade sandboxing and security checks are omitted—do not execute untrusted commands in production without additional safeguards.

import { spawn } from "node:child_process";
import process from "node:process";
import OpenAI from "openai";

const client = new OpenAI();
const MAX_TIMEOUT_MS = 10_000;

function runCommand(command, options) {
  return new Promise((resolve) => {
    let stdout = "";
    let stderr = "";
    let settled = false;
    let groupPoll;
    const child = spawn(command[0], command.slice(1), {
      ...options,
      detached: process.platform !== "win32",
      stdio: ["ignore", "pipe", "pipe"],
    });
    const finish = (suffix = "") => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      clearTimeout(groupPoll);
      resolve(stdout + stderr + suffix);
    };
    const processGroupIsRunning = () => {
      if (process.platform === "win32" || !child.pid) return false;
      try {
        process.kill(-child.pid, 0);
        return true;
      } catch {
        return false;
      }
    };
    const finishAfterProcessGroup = (suffix) => {
      if (settled) return;
      if (processGroupIsRunning()) {
        groupPoll = setTimeout(() => finishAfterProcessGroup(suffix), 10);
      } else {
        finish(suffix);
      }
    };
    const killProcessTree = () => {
      try {
        if (process.platform !== "win32" && child.pid) {
          process.kill(-child.pid, "SIGKILL");
        } else {
          child.kill("SIGKILL");
        }
      } catch {
        child.kill("SIGKILL");
      }
      child.stdout?.destroy();
      child.stderr?.destroy();
    };
    const timer = setTimeout(() => {
      killProcessTree();
      finish("Command timed out.\n");
    }, options.timeout);

    child.stdout?.on("data", (chunk) => {
      stdout += chunk;
    });
    child.stderr?.on("data", (chunk) => {
      stderr += chunk;
    });
    child.on("error", (error) => {
      finish(`Command failed: ${error.message}.\n`);
    });
    child.on("close", (code, signal) => {
      if (signal) {
        finishAfterProcessGroup(`Command failed with signal ${signal}.\n`);
      } else if (code !== 0) {
        finishAfterProcessGroup(`Command failed with exit code ${code}.\n`);
      } else {
        finishAfterProcessGroup("");
      }
    });
  });
}

let response = await client.responses.create({
  model: "codex-mini-latest",
  tools: [{ type: "local_shell" }],
  parallel_tool_calls: false,
  input: "List files in the current directory.",
});

while (true) {
  const shellCall = response.output.find(
    (item) => item.type === "local_shell_call"
  );
  if (!shellCall) break;

  const { command, env, timeout_ms, user, working_directory } =
    shellCall.action;
  let output;
  if (user) {
    output = `Unsupported execution user: ${user}.\n`;
  } else if (command.length === 0) {
    output = "Command is empty.\n";
  } else {
    const timeout =
      timeout_ms && timeout_ms > 0
        ? Math.min(timeout_ms, MAX_TIMEOUT_MS)
        : MAX_TIMEOUT_MS;
    try {
      output = await runCommand(command, {
        cwd: working_directory ?? process.cwd(),
        env: { PATH: process.env.PATH ?? "", ...env },
        timeout,
      });
    } catch (error) {
      output = `Command failed: ${error instanceof Error ? error.message : String(error)}.\n`;
    }
  }

  response = await client.responses.create({
    model: "codex-mini-latest",
    tools: [{ type: "local_shell" }],
    parallel_tool_calls: false,
    previous_response_id: response.id,
    input: [
      {
        type: "local_shell_call_output",
        id: shellCall.call_id,
        output,
      },
    ],
  });
}

console.log(response.output_text);

Best practices

  • Sandbox or containerize execution. Consider using Docker or a jailed user account.
  • Impose resource limits (time, memory, network). The timeout_ms provided by the model is only a hint—you should enforce your own limits.
  • Filter or scrutinize high-risk commands (for example, rm, curl, network utilities).
  • Log every command and its output for auditing and debugging.

Error handling

If the command fails on your side, for example, with a non-zero exit code or timeout, you can still send a local_shell_call_output; include the error message in the output field.

The model can choose to recover or try executing a different command. If you send malformed data (for example, a missing id) the API returns a standard 400 validation error.