Skip to content

OpenAI

OpenAI ships an official CLI for its REST API

7 min read

Written by an AI model from the linked sources and checked against them by a second model. How we work

Image: github.com

On October 21, OpenAI published an official command-line client for its REST API, written in Go and released under Apache 2.0. Its README on GitHub is mostly about defaults: what the tool prints, where it saves files, and what a leading @ means in a request body.

You can install it with brew install openai/tools/openai, or with go install, which needs Go 1.26 or later and puts the binary in $HOME/go/bin. One possible collision: the CLI documentation notes that older versions of the Python SDK also installed a command called openai. If that package is still on the machine, your shell may run the old binary instead of the new one. Fresh installs aren’t affected. The docs pitch the tool at work that suits a terminal: generating images or speech as local files, extracting structured data into JSONL, calling Responses with files or web context, and creating projects and API keys through the Admin API. For work that needs judgment, such as exploring code, debugging or reviewing changes, they point to subagents instead.

Commands follow the API’s resources, in the form openai <resource> [subresource...] <command> [flags...]. For example, openai responses create --input "Say this is a test" --model gpt-5.5, or openai admin organization usage completions with a time window and a bucket width. The older colon spelling, openai admin:organization:usage, still works. Help pages need neither an API key nor a network connection. Running a bare openai lists the command groups by task.

Readable text, even in a pipe

The choice most likely to break a script is the output format. Successful JSON responses print as readable text by default, and that includes output sent into a pipe, which is an unusual default for a tool that scripts will call. The README tells anyone who parses API data to pass --format json or --format jsonl. The full list of formats is auto, text, explore, json, jsonl, pretty, raw and yaml. In auto mode the CLI picks readable text unless you are extracting data.

To extract data you use --transform, which takes a GJSON path, a compact query syntax for picking fields out of JSON. The docs warn against assuming the assistant’s reply is output[0], because a Responses result can put reasoning items ahead of the message. They suggest selecting it by type instead: --transform 'output.#(type=="message").content.0.text'.

Errors get their own channel. By default the CLI writes a short, actionable summary to stderr, with rejected values and request URLs left out. --format-error json returns the original API error, including unknown fields and stream error events reported by the SDK, and --transform-error extracts from it separately from the normal output. If you set --format to json, jsonl, raw or yaml explicitly, that format applies to errors too, unless --format-error overrides it. The README warns that full error details can contain sensitive data supplied by the server. --debug logs whole requests and responses, bodies included, and the README says not to share those logs when payloads are sensitive.

What the @ sign does

In JSON input values, @myfile.ext reads a local file, and that also works inside JSON or YAML passed on stdin. The CLI checks the file type to decide whether to send it as a plain string or as base64. @file:// forces a string and @data:// forces base64. An absolute path gets a third slash, as in @file:///tmp/file.txt. To send a literal string that starts with @, escape it as '\@abe'.

That convenience is a risk when the JSON comes from someone else. A crafted payload could tell the CLI to read a local file and send it to the API. For that case the README adds OPENAI_UNTRUSTED_STDIN=1. With it set, @, @file:// and @data:// references arriving through stdin stay literal text in request bodies, headers and query parameters. File uploads then have to be passed as explicit command-line flags, and those flags take precedence over stdin.

Binary uploads follow a different rule: they take a plain path. openai files upload "upload space.txt" --purpose user_data uploads that file, and --file "@upload space.txt" means a file whose name really starts with @. You can then inspect it with files get and fetch it with files download --output, or redirect to a file with >. The older create, retrieve and content commands are still there.

Images come with presets

openai images generate --prompt "A tiny orange robot" saves the result to ~/Downloads/gpt-images/ and prints the paths, again even in a pipe. Unless you name a model or a legacy response format, the CLI uses gpt-image-2.5-sunburst. --model, --name and --output-dir change the model, the file name and the target directory, which must already exist. Existing files are never overwritten. --format json returns the raw API data, skips saving and doesn’t apply the CLI’s preset. images edit also saves new results and leaves the source files alone.

images create-variation has been retired. To make a variation now, you call images edit with a GPT Image model and a prompt. openai images preview shows a saved image again without an API request. openai images inline on or off sets whether future images appear in the terminal automatically, and a per-command --inline overrides that setting.

One side effect comes before any of this. The first eligible interactive run quietly saves Tab shortcuts for the image picker into future Bash, zsh and fish sessions, and the README links an opt-out. PowerShell keeps its normal completion: you press Enter after openai images generate to open the picker. OpenAI’s other terminal tool has changed things on first run before: Codex CLI 0.157.0 starts a background daemon that keeps running after the session ends. Tab completion also now covers help topics and file paths, but shells that already loaded the old completion script keep the old behavior until it is reloaded.

Keys, headers and mutual TLS

Standard endpoints read OPENAI_API_KEY, and admin endpoints read OPENAI_ADMIN_KEY. The SDK layer picks one or the other depending on the endpoint called. Organization, project, webhook secret and certificate paths each have their own variable and matching flag. --base-url and OPENAI_BASE_URL point the CLI at another compatible backend, which the docs say may support a different set of models or only part of the API. HTTPS is required unless the host is localhost or a loopback address such as 127.0.0.1 or ::1.

Extra headers go in with --header or -H, before or after the resource command. Values are taken literally, so commas and colons are kept and @ does not read a file. If a header name is repeated, the last value wins, regardless of case, and it overrides default and endpoint-specific headers. Header values are hidden in help and redacted in debug logs. For headers that carry credentials, the README recommends having a secret manager or trusted launcher set OPENAI_CUSTOM_HEADERS, one Name: Value per line, so secrets stay out of command arguments and shell history.

Mutual TLS is in beta. It means the client proves its identity with its own certificate as well as checking the server’s. Organizations opt in through OpenAI’s beta program and activate a CA certificate. Then you provide the client certificate and private key as two separate PEM files, set together. If certificate-chain support is on, the leaf certificate comes first, followed by intermediates. The CLI doesn’t switch endpoints by itself, so you set the base URL to https://mtls.api.openai.com/v1 explicitly. HTTP and SOCKS proxies still work, but HTTPS proxies are rejected so the client certificate isn’t presented during the proxy’s own TLS handshake.

Bug reports and feature requests go through GitHub issues, but code contributions are closed to outsiders: “Pull requests are limited to repository collaborators; we do not accept pull requests from non-collaborators.”

No comments yet. Be the first.

Join the conversation

Sign in to leave a comment. It appears after moderation.