Repository Public
1# Local shell23> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.45The local shell tool is outdated. For new use cases, use the6 [`shell`](https://developers.openai.com/api/docs/guides/tools-shell) tool with GPT-5.1 instead. [Learn7 more](https://developers.openai.com/api/docs/guides/tools-shell).89Local 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](https://github.com/openai/codex) and [`codex-mini-latest`](https://developers.openai.com/api/docs/models/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.1011Local shell is available through the [Responses API](https://developers.openai.com/api/docs/guides/migrate-to-responses) for use with [`codex-mini-latest`](https://developers.openai.com/api/docs/models/codex-mini-latest). It is not available on other models or via the Chat Completions API.1213Running arbitrary shell commands can be dangerous. Always sandbox execution14or add strict allowlists or deny lists before forwarding a command to the system15shell.16171819See [Codex CLI](https://github.com/openai/codex) for reference implementation.2021## How it works2223The local shell tool enables agents to run in a continuous loop with access to a terminal.2425The 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.2627Your 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.28293031Integrating the local shell tool32333435These are the high-level steps you need to follow to integrate the local shell tool in your application:36371. **Send a request to the model**:38 Include the `local_shell` tool as part of the available tools.39402. **Receive a response from the model**:41 Check if the response has any `local_shell_call` items.42 This tool call contains an action like `exec` with a command to execute.43443. **Execute the requested action**:45 Run the command in the local environment you control.46474. **Return the action output**:48 After executing the action, return the command output to the model.49505. **Repeat**:51 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.5253## Example workflow5455Below is a minimal example showing the request/response loop. Choose a language56to see the equivalent workflow for its SDK. For brevity, production-grade57sandboxing and security checks are omitted—**do not execute untrusted commands58in production without additional safeguards**.5960```javascript61import { spawn } from "node:child_process";62import process from "node:process";63import OpenAI from "openai";6465const client = new OpenAI();66const MAX_TIMEOUT_MS = 10_000;6768function runCommand(command, options) {69 return new Promise((resolve) => {70 let stdout = "";71 let stderr = "";72 let settled = false;73 let groupPoll;74 const child = spawn(command[0], command.slice(1), {75 ...options,76 detached: process.platform !== "win32",77 stdio: ["ignore", "pipe", "pipe"],78 });79 const finish = (suffix = "") => {80 if (settled) return;81 settled = true;82 clearTimeout(timer);83 clearTimeout(groupPoll);84 resolve(stdout + stderr + suffix);85 };86 const processGroupIsRunning = () => {87 if (process.platform === "win32" || !child.pid) return false;88 try {89 process.kill(-child.pid, 0);90 return true;91 } catch {92 return false;93 }94 };95 const finishAfterProcessGroup = (suffix) => {96 if (settled) return;97 if (processGroupIsRunning()) {98 groupPoll = setTimeout(() => finishAfterProcessGroup(suffix), 10);99 } else {100 finish(suffix);101 }102 };103 const killProcessTree = () => {104 try {105 if (process.platform !== "win32" && child.pid) {106 process.kill(-child.pid, "SIGKILL");107 } else {108 child.kill("SIGKILL");109 }110 } catch {111 child.kill("SIGKILL");112 }113 child.stdout?.destroy();114 child.stderr?.destroy();115 };116 const timer = setTimeout(() => {117 killProcessTree();118 finish("Command timed out.\n");119 }, options.timeout);120121 child.stdout?.on("data", (chunk) => {122 stdout += chunk;123 });124 child.stderr?.on("data", (chunk) => {125 stderr += chunk;126 });127 child.on("error", (error) => {128 finish(`Command failed: ${error.message}.\n`);129 });130 child.on("close", (code, signal) => {131 if (signal) {132 finishAfterProcessGroup(`Command failed with signal ${signal}.\n`);133 } else if (code !== 0) {134 finishAfterProcessGroup(`Command failed with exit code ${code}.\n`);135 } else {136 finishAfterProcessGroup("");137 }138 });139 });140}141142let response = await client.responses.create({143 model: "codex-mini-latest",144 tools: [{ type: "local_shell" }],145 parallel_tool_calls: false,146 input: "List files in the current directory.",147});148149while (true) {150 const shellCall = response.output.find(151 (item) => item.type === "local_shell_call"152 );153 if (!shellCall) break;154155 const { command, env, timeout_ms, user, working_directory } =156 shellCall.action;157 let output;158 if (user) {159 output = `Unsupported execution user: ${user}.\n`;160 } else if (command.length === 0) {161 output = "Command is empty.\n";162 } else {163 const timeout =164 timeout_ms && timeout_ms > 0165 ? Math.min(timeout_ms, MAX_TIMEOUT_MS)166 : MAX_TIMEOUT_MS;167 try {168 output = await runCommand(command, {169 cwd: working_directory ?? process.cwd(),170 env: { PATH: process.env.PATH ?? "", ...env },171 timeout,172 });173 } catch (error) {174 output = `Command failed: ${error instanceof Error ? error.message : String(error)}.\n`;175 }176 }177178 response = await client.responses.create({179 model: "codex-mini-latest",180 tools: [{ type: "local_shell" }],181 parallel_tool_calls: false,182 previous_response_id: response.id,183 input: [184 {185 type: "local_shell_call_output",186 id: shellCall.call_id,187 output,188 },189 ],190 });191}192193console.log(response.output_text);194```195196```python197import os198import signal199import subprocess200import time201from contextlib import suppress202from openai import OpenAI203204client = OpenAI()205MAX_TIMEOUT_MS = 10_000206207208def output_text(value):209 if isinstance(value, bytes):210 return value.decode(errors="replace")211 return value or ""212213214def process_group_is_running(pid):215 if os.name == "nt":216 return False217 try:218 os.killpg(pid, 0)219 return True220 except ProcessLookupError:221 return False222 except PermissionError:223 return True224225226response = client.responses.create(227 model="codex-mini-latest",228 tools=[{"type": "local_shell"}],229 parallel_tool_calls=False,230 input="List files in the current directory.",231)232233while True:234 shell_call = next(235 (item for item in response.output if item.type == "local_shell_call"),236 None,237 )238 if shell_call is None:239 break240241 action = shell_call.action242 if action.user:243 output = f"Unsupported execution user: {action.user}.\n"244 elif not action.command:245 output = "Command is empty.\n"246 else:247 timeout_ms = (248 min(action.timeout_ms, MAX_TIMEOUT_MS)249 if action.timeout_ms and action.timeout_ms > 0250 else MAX_TIMEOUT_MS251 )252 deadline = time.monotonic() + timeout_ms / 1000253 try:254 process = subprocess.Popen(255 action.command,256 cwd=action.working_directory or os.getcwd(),257 env={"PATH": os.environ.get("PATH", ""), **action.env},258 stdin=subprocess.DEVNULL,259 stdout=subprocess.PIPE,260 stderr=subprocess.PIPE,261 text=True,262 errors="replace",263 start_new_session=True,264 )265 stdout, stderr = process.communicate(266 timeout=max(deadline - time.monotonic(), 0)267 )268 while process_group_is_running(process.pid):269 remaining = deadline - time.monotonic()270 if remaining <= 0:271 raise subprocess.TimeoutExpired(action.command, timeout_ms / 1000)272 time.sleep(min(remaining, 0.01))273 output = stdout + stderr274 if process.returncode:275 output += f"Command failed with exit code {process.returncode}.\n"276 except subprocess.TimeoutExpired as error:277 if os.name == "nt":278 process.kill()279 else:280 with suppress(ProcessLookupError):281 os.killpg(process.pid, signal.SIGKILL)282 try:283 stdout, stderr = process.communicate(284 timeout=max(deadline - time.monotonic(), 0)285 )286 except subprocess.TimeoutExpired as drain_error:287 if process.stdout:288 process.stdout.close()289 if process.stderr:290 process.stderr.close()291 stdout = output_text(292 drain_error.stdout293 if drain_error.stdout is not None294 else error.stdout295 )296 stderr = output_text(297 drain_error.stderr298 if drain_error.stderr is not None299 else error.stderr300 )301 output = output_text(stdout) + output_text(stderr) + "Command timed out.\n"302 except (OSError, TypeError, ValueError) as error:303 output = f"Command failed: {error}.\n"304305 output_item = {306 "type": "local_shell_call_output",307 "id": shell_call.call_id,308 "output": output,309 }310311 response = client.responses.create(312 model="codex-mini-latest",313 tools=[{"type": "local_shell"}],314 parallel_tool_calls=False,315 previous_response_id=response.id,316 input=[output_item],317 )318319print(response.output_text)320```321322```go323package main324325import (326 "bytes"327 "context"328 "errors"329 "fmt"330 "io"331 "os"332 "os/exec"333 "path/filepath"334 "sync"335 "sync/atomic"336 "syscall"337 "time"338339 "github.com/openai/openai-go/v3"340 "github.com/openai/openai-go/v3/responses"341)342343const maxCommandTimeout = 10 * time.Second344345func main() {346 client := openai.NewClient()347 tool := responses.ToolUnionParam{OfLocalShell: &responses.ToolLocalShellParam{}}348 response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{349 Model: "codex-mini-latest",350 Tools: []responses.ToolUnionParam{tool},351 ParallelToolCalls: openai.Bool(false),352 Input: responses.ResponseNewParamsInputUnion{353 OfString: openai.String("List files in the current directory."),354 },355 })356 if err != nil {357 panic(err)358 }359360 for {361 var shellCall *responses.ResponseOutputItemLocalShellCall362 for _, item := range response.Output {363 if item.Type == "local_shell_call" {364 call := item.AsLocalShellCall()365 shellCall = &call366 break367 }368 }369 if shellCall == nil {370 break371 }372373 action := shellCall.Action374 var output []byte375 if action.User != "" {376 output = []byte(fmt.Sprintf("Unsupported execution user: %s.\n", action.User))377 } else if len(action.Command) == 0 {378 output = []byte("Command is empty.\n")379 } else {380 path := os.Getenv("PATH")381 if actionPath, ok := action.Env["PATH"]; ok {382 path = actionPath383 }384 executable, pathErr := commandPath(action.Command[0], path, action.WorkingDirectory)385 if pathErr != nil {386 output = []byte(fmt.Sprintf("Command failed: %v\n", pathErr))387 } else {388 timeout := maxCommandTimeout389 if action.TimeoutMs > 0 && action.TimeoutMs < maxCommandTimeout.Milliseconds() {390 timeout = time.Duration(action.TimeoutMs) * time.Millisecond391 }392 deadline := time.Now().Add(timeout)393 ctx, cancel := context.WithTimeout(context.Background(), timeout)394 command := exec.CommandContext(ctx, executable, action.Command[1:]...)395 command.Args[0] = action.Command[0]396 command.Dir = action.WorkingDirectory397 command.Env = []string{"PATH=" + path}398 command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}399 for key, value := range action.Env {400 if key == "PATH" {401 continue402 }403 command.Env = append(command.Env, key+"="+value)404 }405 killProcessGroup := func() {406 if command.Process != nil {407 _ = syscall.Kill(-command.Process.Pid, syscall.SIGKILL)408 }409 }410 processGroupIsRunning := func() bool {411 return command.Process != nil && syscall.Kill(-command.Process.Pid, 0) == nil412 }413 stdout, stdoutWriter, stdoutErr := os.Pipe()414 stderr, stderrWriter, stderrErr := os.Pipe()415 if stdoutErr != nil || stderrErr != nil {416 if stdout != nil {417 _ = stdout.Close()418 }419 if stdoutWriter != nil {420 _ = stdoutWriter.Close()421 }422 if stderr != nil {423 _ = stderr.Close()424 }425 if stderrWriter != nil {426 _ = stderrWriter.Close()427 }428 output = []byte(fmt.Sprintf("Command failed: %v%v\n", stdoutErr, stderrErr))429 } else {430 command.Stdout = stdoutWriter431 command.Stderr = stderrWriter432 var combinedOutput bytes.Buffer433 var outputLock sync.Mutex434 var readers sync.WaitGroup435 readOutput := func(reader io.ReadCloser) {436 defer readers.Done()437 data, _ := io.ReadAll(reader)438 outputLock.Lock()439 _, _ = combinedOutput.Write(data)440 outputLock.Unlock()441 }442 var commandErr error443 commandErr = command.Start()444 if commandErr == nil {445 _ = stdoutWriter.Close()446 _ = stderrWriter.Close()447 readers.Add(2)448 go readOutput(stdout)449 go readOutput(stderr)450 var timedOut atomic.Bool451 remaining := time.Until(deadline)452 if remaining < 0 {453 remaining = 0454 }455 markTimedOut := func() {456 if timedOut.Swap(true) {457 return458 }459 killProcessGroup()460 _ = stdout.Close()461 _ = stderr.Close()462 }463 timer := time.AfterFunc(remaining, markTimedOut)464 commandErr = command.Wait()465 if !time.Now().Before(deadline) ||466 errors.Is(commandErr, context.DeadlineExceeded) ||467 errors.Is(ctx.Err(), context.DeadlineExceeded) {468 markTimedOut()469 }470 for processGroupIsRunning() && !timedOut.Load() {471 time.Sleep(10 * time.Millisecond)472 }473 readers.Wait()474 if !timer.Stop() || !time.Now().Before(deadline) {475 markTimedOut()476 }477 output = combinedOutput.Bytes()478 if timedOut.Load() || errors.Is(ctx.Err(), context.DeadlineExceeded) {479 killProcessGroup()480 output = append(output, "Command timed out.\n"...)481 } else if commandErr != nil {482 output = append(output, fmt.Sprintf("Command failed: %v\n", commandErr)...)483 }484 } else {485 _ = stdout.Close()486 _ = stderr.Close()487 _ = stdoutWriter.Close()488 _ = stderrWriter.Close()489 output = append(output, fmt.Sprintf("Command failed: %v\n", commandErr)...)490 }491 }492 cancel()493 }494 }495496 response, err = client.Responses.New(context.Background(), responses.ResponseNewParams{497 Model: "codex-mini-latest",498 Tools: []responses.ToolUnionParam{tool},499 ParallelToolCalls: openai.Bool(false),500 PreviousResponseID: openai.String(response.ID),501 Input: responses.ResponseNewParamsInputUnion{502 OfInputItemList: []responses.ResponseInputItemUnionParam{{503 OfLocalShellCallOutput: &responses.ResponseInputItemLocalShellCallOutputParam{504 ID: shellCall.CallID,505 Output: string(output),506 },507 }},508 },509 })510 if err != nil {511 panic(err)512 }513 }514515 fmt.Println(response.OutputText())516}517518func commandPath(command string, path string, workingDirectory string) (string, error) {519 if filepath.Base(command) != command {520 return command, nil521 }522 baseDirectory, err := filepath.Abs(workingDirectory)523 if err != nil {524 return "", err525 }526 directories := filepath.SplitList(path)527 if len(directories) == 0 {528 directories = []string{""}529 }530 for _, directory := range directories {531 if directory == "" {532 directory = "."533 }534 if !filepath.IsAbs(directory) {535 directory = filepath.Join(baseDirectory, directory)536 }537 candidate := filepath.Join(directory, command)538 info, err := os.Stat(candidate)539 if err == nil && !info.IsDir() && info.Mode()&0o111 != 0 {540 return candidate, nil541 }542 }543 return "", fmt.Errorf("command %q not found in PATH", command)544}545```546547```java548import com.openai.client.OpenAIClient;549import com.openai.client.okhttp.OpenAIOkHttpClient;550import com.openai.core.JsonValue;551import com.openai.models.responses.ResponseCreateParams;552import com.openai.models.responses.ResponseInputItem;553import java.io.IOException;554import java.io.UncheckedIOException;555import java.nio.charset.StandardCharsets;556import java.nio.file.Files;557import java.nio.file.Path;558import java.util.ArrayList;559import java.util.LinkedHashMap;560import java.util.List;561import java.util.Locale;562import java.util.Map;563import java.util.concurrent.CompletableFuture;564import java.util.concurrent.TimeUnit;565import java.util.concurrent.TimeoutException;566567ResponseCreateParams.Builder request =568 ResponseCreateParams.builder()569 .model("codex-mini-latest")570 .input("List files in the current directory.")571 .parallelToolCalls(false)572 .putAdditionalBodyProperty(573 "tools", JsonValue.from(List.of(Map.of("type", "local_shell"))));574var response = client.responses().create(request.build());575576while (true) {577 var shellCall =578 response.output().stream()579 .flatMap(item -> item.localShellCall().stream())580 .findFirst()581 .orElse(null);582 if (shellCall == null) {583 break;584 }585586 var action = shellCall.action();587 String output;588 if (action.user().isPresent()) {589 output = "Unsupported execution user: " + action.user().get() + ".\n";590 } else if (action.command().isEmpty()) {591 output = "Command is empty.\n";592 } else {593 try {594 boolean usesShellSupervisor =595 !System.getProperty("os.name").toLowerCase(Locale.ROOT).startsWith("win");596 String hostPath = System.getenv("PATH");597 String childPath = hostPath;598 Map<String, String> actionEnvironment = new LinkedHashMap<>();599 for (Map.Entry<String, com.openai.core.JsonValue> variable :600 action.env()._additionalProperties().entrySet()) {601 String value = (String) variable.getValue().asString().orElseThrow();602 actionEnvironment.put(variable.getKey(), value);603 if (variable.getKey().equals("PATH")) {604 childPath = value;605 }606 }607 List<String> command;608 if (usesShellSupervisor) {609 String supervisorShell =610 Files.isExecutable(Path.of("/bin/bash")) ? "/bin/bash" : "/bin/sh";611 command =612 new ArrayList<>(613 List.of(614 supervisorShell,615 "-c",616 "set -m; child=; "617 + "cleanup() { test -z \"$child\" || "618 + "kill -KILL -- \"-$child\" 2>/dev/null; }; "619 + "trap cleanup TERM INT HUP; \"$@\" & child=$!; set +m; "620 + "wait \"$child\" 2>/dev/null; status=$?; "621 + "while kill -0 -- \"-$child\" 2>/dev/null; do sleep 0.01; done; "622 + "exit \"$status\"",623 "local-shell",624 "/usr/bin/env",625 "-i"));626 if (childPath != null) {627 command.add("PATH=" + childPath);628 }629 for (Map.Entry<String, String> variable : actionEnvironment.entrySet()) {630 if (!variable.getKey().equals("PATH")) {631 command.add(variable.getKey() + "=" + variable.getValue());632 }633 }634 command.addAll(action.command());635 } else {636 command = new ArrayList<>(action.command());637 }638 ProcessBuilder processBuilder = new ProcessBuilder(command);639 processBuilder.directory(action.workingDirectory().map(java.io.File::new).orElse(null));640 processBuilder.environment().clear();641 if (hostPath != null) {642 processBuilder.environment().put("PATH", hostPath);643 }644 if (!usesShellSupervisor) {645 processBuilder.environment().putAll(actionEnvironment);646 }647 Process process = processBuilder.redirectErrorStream(true).start();648 process.getOutputStream().close();649 var outputFuture =650 CompletableFuture.supplyAsync(651 () -> {652 try {653 return new String(654 process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);655 } catch (IOException error) {656 throw new UncheckedIOException(error);657 }658 });659 long timeoutMillis =660 action661 .timeoutMs()662 .filter(timeout -> timeout > 0)663 .map(timeout -> Math.min(timeout, MAX_TIMEOUT_MILLIS))664 .orElse(MAX_TIMEOUT_MILLIS);665 long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis);666 boolean finished = process.waitFor(timeoutMillis, TimeUnit.MILLISECONDS);667 if (!finished) {668 destroyProcessTree(process, usesShellSupervisor);669 }670 try {671 long remainingNanos = Math.max(1, deadlineNanos - System.nanoTime());672 output = outputFuture.get(remainingNanos, TimeUnit.NANOSECONDS);673 if (!finished) {674 output = "Command timed out.\n" + output;675 } else if (process.exitValue() != 0) {676 output += "Command failed with exit code " + process.exitValue() + ".\n";677 }678 } catch (TimeoutException error) {679 destroyProcessTree(process, usesShellSupervisor);680 process.getInputStream().close();681 output = "Command timed out.\n";682 } catch (java.util.concurrent.ExecutionException error) {683 output = "Command failed: " + error.getCause().getMessage() + ".\n";684 }685 } catch (IOException | IllegalArgumentException error) {686 output = "Command failed: " + error.getMessage() + ".\n";687 }688 }689690 response =691 client692 .responses()693 .create(694 request695 .previousResponseId(response.id())696 .inputOfResponse(697 List.of(698 ResponseInputItem.ofLocalShellCallOutput(699 ResponseInputItem.LocalShellCallOutput.builder()700 .id(shellCall.callId())701 .output(output)702 .build())))703 .build());704}705706response.output().stream()707 .flatMap(item -> item.message().stream())708 .flatMap(message -> message.content().stream())709 .flatMap(content -> content.outputText().stream())710 .forEach(text -> System.out.println(text.text()));711712private static void destroyProcessTree(Process process, boolean usesShellSupervisor) {713 process.descendants().forEach(ProcessHandle::destroyForcibly);714 if (usesShellSupervisor) {715 process.destroy();716 } else {717 process.destroyForcibly();718 }719}720```721722```ruby723require "open3"724require "openai"725require "timeout"726727client = OpenAI::Client.new728MAX_TIMEOUT_MS = 10_000729response = client.responses.create(730 model: "codex-mini-latest",731 tools: [{ type: :local_shell }],732 parallel_tool_calls: false,733 input: "List files in the current directory."734)735736loop do737 shell_call = response.output.find do |item|738 item.is_a?(OpenAI::Models::Responses::ResponseOutputItem::LocalShellCall)739 end740 break unless shell_call.is_a?(741 OpenAI::Models::Responses::ResponseOutputItem::LocalShellCall742 )743744 action = shell_call.action745 stdout = +""746 stderr = +""747 if action.user748 stderr << "Unsupported execution user: #{action.user}.\n"749 elsif action.command.empty?750 stderr << "Command is empty.\n"751 else752 begin753 executable = action.command.fetch(0)754 environment = { "PATH" => ENV.fetch("PATH", "") }.merge(action.env.transform_keys(&:to_s))755 status, timed_out = Open3.popen3(756 environment,757 [executable, executable],758 *action.command.drop(1),759 chdir: action.working_directory || Dir.pwd,760 pgroup: true,761 unsetenv_others: true762 ) do |stdin, child_stdout, child_stderr, wait_thread|763 stdin.close764 stdout_reader = Thread.new {765 begin766 child_stdout.read767 rescue768 ""769 end770 }771 stderr_reader = Thread.new {772 begin773 child_stderr.read774 rescue775 ""776 end777 }778 timeout_ms = action.timeout_ms779 timeout = if timeout_ms&.positive?780 [timeout_ms, MAX_TIMEOUT_MS].min / 1000.0781 else782 MAX_TIMEOUT_MS / 1000.0783 end784 deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout785786 command_timed_out = false787 wait_status = begin788 status = Timeout.timeout(timeout) { wait_thread.value }789 remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)790 raise Timeout::Error if remaining <= 0791792 stdout << Timeout.timeout(remaining) { stdout_reader.value }793 remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)794 raise Timeout::Error if remaining <= 0795796 stderr << Timeout.timeout(remaining) { stderr_reader.value }797 group_running = proc do798 Process.kill(0, -wait_thread.pid)799 true800 rescue Errno::ESRCH801 false802 rescue Errno::EPERM803 true804 end805 while group_running.call806 remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)807 raise Timeout::Error if remaining <= 0808809 sleep [remaining, 0.01].min810 end811 status812 rescue Timeout::Error813 command_timed_out = true814 begin815 Process.kill("TERM", -wait_thread.pid)816 Process.kill("KILL", -wait_thread.pid)817 rescue Errno::ESRCH818 nil819 end820 child_stdout.close821 child_stderr.close822 stdout_reader.kill823 stderr_reader.kill824 stderr << "Command timed out.\n"825 wait_thread.value826 end827 [wait_status, command_timed_out]828 end829 exit_status = status.exitstatus830 if exit_status && !status.success? && !timed_out831 stderr << "Command failed with exit code #{exit_status}.\n"832 elsif status.signaled? && !timed_out833 stderr << "Command failed with signal #{status.termsig}.\n"834 end835 rescue SystemCallError, ArgumentError, TypeError => error836 stderr << "Command failed: #{error.message}.\n"837 end838 end839840 response = client.responses.create(841 model: "codex-mini-latest",842 tools: [{ type: :local_shell }],843 parallel_tool_calls: false,844 previous_response_id: response.id,845 input: [846 {847 type: :local_shell_call_output,848 id: shell_call.call_id,849 output: (stdout + stderr).encode("UTF-8", invalid: :replace, undef: :replace)850 }851 ]852 )853end854855puts(response.output_text)856```857858859## Best practices860861- **Sandbox or containerize** execution. Consider using Docker or a jailed user862 account.863- **Impose resource limits** (time, memory, network). The `timeout_ms`864 provided by the model is only a hint—you should enforce your own limits.865- **Filter or scrutinize** high-risk commands (for example, `rm`, `curl`, network866 utilities).867- **Log every command and its output** for auditing and debugging.868869### Error handling870871If 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.872873The 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.