🌍 Global Mirror — Visit original CN site →
Skip to main content

⚡ Open Terminal

info

This page is up-to-date with Open Terminal release version v0.2.4.

Overview

Open Terminal is a lightweight API for running shell commands remotely — with background process management, file operations, and secure access. When connected to Open WebUI as a Tool, it gives models full shell access, a complete file system toolkit, and the ability to execute arbitrary commands in an isolated environment.

Unlike Pyodide (browser-based, limited libraries) or Jupyter (shared environment, no per-user isolation), Open Terminal runs in its own Docker container with full OS-level capabilities. This makes it ideal for tasks that require:

  • Installing and running any software package or language
  • Working with system tools like ffmpeg, pandoc, git, etc.
  • Reading, writing, and editing files directly
  • Running multi-step build, analysis, or data processing pipelines
  • Interacting with REPLs and interactive commands via stdin
warning

Open Terminal provides unrestricted shell access to the environment it runs in. In production deployments, always run it inside a Docker container with appropriate resource limits. Never run it on bare metal in a shared or untrusted environment.

Getting Started

docker run -d --name open-terminal --restart unless-stopped -p 8000:8000 -v open-terminal:/home/user -e OPEN_TERMINAL_API_KEY=your-secret-key ghcr.io/open-webui/open-terminal

The -v open-terminal:/home/user flag creates a named volume so files in the user's home directory survive container restarts. If no API key is provided, one is auto-generated and printed on startup (docker logs open-terminal).

The container runs as a non-root user (user) with passwordless sudo available when elevated privileges are needed. The working directory is /home/user.

The Docker image is based on Python 3.12 and comes pre-installed with a rich set of tools:

CategoryIncluded
Core utilitiescoreutils, findutils, grep, sed, gawk, diffutils, patch, less, file, tree, bc
Networkingcurl, wget, net-tools, iputils-ping, dnsutils, netcat, socat, telnet, openssh-client, rsync
Editorsvim, nano
Version controlgit
Build toolsbuild-essential, cmake, make
LanguagesPython 3.12, Perl, Ruby, Lua 5.4
Data processingjq, xmlstarlet, sqlite3
Compressionzip, unzip, tar, gzip, bzip2, xz, zstd, p7zip
Systemprocps, htop, lsof, strace, sysstat, sudo, tmux, screen
Python librariesnumpy, pandas, scipy, scikit-learn, matplotlib, seaborn, plotly, jupyter, ipython, requests, beautifulsoup4, lxml, sqlalchemy, psycopg2, pyyaml, toml, jsonlines, tqdm, rich

Build from Source

docker build -t open-terminal .
docker run -p 8000:8000 open-terminal

pip Install (Bare Metal)

# One-liner with uvx (no install needed)
uvx open-terminal run --host 0.0.0.0 --port 8000 --api-key your-secret-key

# Or install globally with pip
pip install open-terminal
open-terminal run --host 0.0.0.0 --port 8000 --api-key your-secret-key
warning

Running bare metal gives the model shell access to your actual machine. Only use this for local development or testing.

MCP Server Mode

Open Terminal can also run as an MCP (Model Context Protocol) server, exposing all its endpoints as MCP tools. This requires an additional dependency:

pip install open-terminal[mcp]

Then start the MCP server:

# stdio transport (default — for local MCP clients)
open-terminal mcp

# streamable-http transport (for remote/networked MCP clients)
open-terminal mcp --transport streamable-http --host 0.0.0.0 --port 8000
OptionDefaultDescription
--transportstdioTransport mode: stdio or streamable-http
--host0.0.0.0Bind address (streamable-http only)
--port8000Bind port (streamable-http only)

Under the hood, this uses FastMCP to automatically convert every FastAPI endpoint into an MCP tool — no manual tool definitions needed.

Docker Compose (with Open WebUI)

docker-compose.yml
services:
open-webui:
image: ghcr.io/open-webui/open-webui:latest
container_name: open-webui
ports:
- "3000:8080"
volumes:
- open-webui:/app/backend/data

open-terminal:
image: ghcr.io/open-webui/open-terminal
container_name: open-terminal
ports:
- "8000:8000"
volumes:
- open-terminal:/home/user
environment:
- OPEN_TERMINAL_API_KEY=your-secret-key
deploy:
resources:
limits:
memory: 2G
cpus: "2.0"

volumes:
open-webui:
open-terminal:

Configuration

CLI OptionDefaultEnvironment VariableDescription
--host0.0.0.0Bind address
--port8000Bind port
--api-keyAuto-generatedOPEN_TERMINAL_API_KEYBearer API key for authentication
~/.open-terminal/logsOPEN_TERMINAL_LOG_DIRDirectory for process JSONL log files

When no API key is provided, Open Terminal generates a random key using a cryptographically secure token and prints it to the console on startup.

Process output is persisted to JSONL log files under OPEN_TERMINAL_LOG_DIR/processes/. These files provide a full audit trail that survives process cleanup and server restarts.

Performance

As of v0.2.4, all file and upload endpoints use fully async I/O via aiofiles. Directory listing and file search operations run in a background thread via asyncio.to_thread. This means the server's event loop is never blocked by filesystem operations, even on large directories or slow storage.

Connecting to Open WebUI

Open Terminal is a FastAPI application and automatically exposes an OpenAPI specification at /openapi.json. This means it works out of the box as an OpenAPI Tool Server — no manual tool creation required.

  • As a User Tool Server: Add it in Settings → Tools to connect directly from your browser. Ideal for personal or local instances.
  • As a Global Tool Server: Add it in Admin Settings → Tools to make it available to all users across the deployment.

For step-by-step instructions with screenshots, see the OpenAPI Tool Server Integration Guide.

API Reference

All endpoints except /health and temporary download/upload links require Bearer token authentication.

Interactive API documentation (Swagger UI) is available at http://localhost:8000/docs when the server is running.

Command Execution

Execute a Command

POST /execute

Runs a shell command as a background process and returns a process ID. All output is persisted to a JSONL log file. Supports pipes, chaining (&&, ||, ;), and redirections.

tip

The /execute endpoint description in the OpenAPI spec automatically includes live system metadata — OS, hostname, current user, default shell, Python version, and working directory. When Open WebUI discovers this tool via the OpenAPI spec, models see this context in the tool description and can adapt their commands accordingly.

Request body:

FieldTypeDefaultDescription
commandstring(required)Shell command to execute
cwdstringServer's working directoryWorking directory for the command
envobjectnullExtra environment variables merged into the subprocess environment

Query parameters:

ParameterDefaultDescription
streamfalseIf true, stream output as JSONL instead of waiting for completion
tail(all)Return only the last N output entries. Useful to limit response size for AI agents.
waitnumbernull
tailintegernull

Example — fire and forget:

curl -X POST http://localhost:8000/execute \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{"command": "echo hello"}'
{
"id": "a1b2c3d4e5f6",
"command": "echo hello",
"status": "running",
"exit_code": null,
"output": [],
"truncated": false,
"next_offset": 0,
"log_path": "/home/user/.open-terminal/logs/processes/a1b2c3d4e5f6.jsonl"
}

Example — wait for completion:

curl -X POST "http://localhost:8000/execute?wait=5" \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{"command": "echo hello"}'
{
"id": "a1b2c3d4e5f6",
"command": "echo hello",
"status": "done",
"exit_code": 0,
"output": [
{"type": "stdout", "data": "hello\n"}
],
"truncated": false,
"next_offset": 1,
"log_path": "/home/user/.open-terminal/logs/processes/a1b2c3d4e5f6.jsonl"
}
File-Backed Process Output

All background process output (stdout/stderr) is persisted to JSONL log files under ~/.open-terminal/logs/processes/. This means output is never lost, even if the server restarts. The response includes next_offset for stateless incremental polling — pass it as the offset query parameter on subsequent status requests to get only new output. The log_path field shows the path to the raw JSONL log file.

Search File Contents

GET /files/search

Search for a text pattern across files in a directory. Returns structured matches with file paths, line numbers, and matching lines. Skips binary files automatically.

Query parameters:

ParameterTypeDefaultDescription
querystring(required)Text or regex pattern to search for
pathstring.Directory or file to search in
regexbooleanfalseTreat query as a regex pattern
case_insensitivebooleanfalsePerform case-insensitive matching
includestring[](all files)Glob patterns to filter files (e.g. *.py). Files must match at least one pattern.
match_per_linebooleantrueIf true, return each matching line with line numbers. If false, return only matching filenames.
max_resultsinteger50Maximum number of matches to return (1–500)
curl "http://localhost:8000/files/search?query=TODO&include=*.py&case_insensitive=true" \

#### Get Command Status

**`GET /execute/{process_id}/status`**

Returns process status, exit code, and output since the last poll. Use `offset` and `next_offset` for stateless incremental reads — multiple clients can independently track the same process without data loss.

| Parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `wait` | number | `null` | Seconds to wait for the process to finish before returning. Returns early if the process exits. |
| `offset` | integer | `0` | Number of output entries to skip. Use `next_offset` from the previous response to get only new output. |
| `tail` | integer | `null` | Return only the last N output entries. |

```bash
curl "http://localhost:8000/execute/a1b2c3d4e5f6/status?offset=0&wait=10" \
-H "Authorization: Bearer <api-key>"
{
"id": "a1b2c3d4e5f6",
"command": "echo hello",
"status": "done",
"exit_code": 0,
"output": [
{"type": "stdout", "data": "hello\n"}
],
"truncated": false,
"next_offset": 1,
"log_path": "/home/user/.open-terminal/logs/processes/a1b2c3d4e5f6.jsonl"
}

List Processes

GET /execute

Returns a list of all tracked background processes, including running, done, and killed.

curl http://localhost:8000/execute \
-H "Authorization: Bearer <api-key>"
[
{
"id": "a1b2c3d4e5f6",
"command": "python train.py",
"status": "running",
"exit_code": null,
"log_path": "/home/user/.open-terminal/logs/processes/a1b2c3d4e5f6.jsonl"
}
]

Finished processes are automatically cleaned up after 5 minutes. Their JSONL log files are retained on disk for audit purposes.

Send Input

POST /execute/{process_id}/input

Sends text to a running process's stdin. Useful for interacting with REPLs, interactive commands, or any process waiting for input.

Request body:

FieldTypeDescription
inputstringText to send to stdin. Include newline characters as needed.
curl -X POST http://localhost:8000/execute/a1b2c3d4e5f6/input \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{"input": "print(42)\n"}'

Kill a Process

DELETE /execute/{process_id}

Terminates a running process. Sends SIGTERM by default for graceful shutdown.

ParameterTypeDefaultDescription
forcebooleanfalseSend SIGKILL instead of SIGTERM.
curl -X DELETE "http://localhost:8000/execute/a1b2c3d4e5f6?force=true" \
-H "Authorization: Bearer <api-key>"

File Operations

List Directory Contents

GET /files/list

Returns a structured listing of files and directories at the given path, including name, type, size, and modification time.

ParameterTypeDefaultDescription
directorystring.Directory path to list.
curl "http://localhost:8000/files/list?directory=/home/user" \
-H "Authorization: Bearer <api-key>"
{
"dir": "/home/user",
"entries": [
{"name": "data.csv", "type": "file", "size": 1024, "modified": 1707955200.0},
{"name": "scripts", "type": "directory", "size": 4096, "modified": 1707955200.0}
]
}

Read a File

GET /files/read

Returns the contents of a file. Text files return a content string; binary files return base64-encoded content. Optionally specify a line range for large text files.

ParameterTypeDefaultDescription
pathstring(required)Path to the file to read.
start_lineintegernullFirst line to return (1-indexed, inclusive).
end_lineintegernullLast line to return (1-indexed, inclusive).
curl "http://localhost:8000/files/read?path=/home/user/script.py&start_line=1&end_line=10" \
-H "Authorization: Bearer <api-key>"
{
"path": "/home/user/script.py",
"total_lines": 50,
"content": "#!/usr/bin/env python3\nimport sys\n..."
}

Write a File

POST /files/write

Writes text content to a file. Creates parent directories automatically. Overwrites if the file already exists.

Request body:

FieldTypeDescription
pathstringAbsolute or relative path. Parent directories are created automatically.
contentstringText content to write.
curl -X POST http://localhost:8000/files/write \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{"path": "/home/user/hello.py", "content": "print(\"hello\")\n"}'

Replace Content in a File

POST /files/replace

Find and replace exact strings in a file. Supports multiple replacements in one call with optional line range narrowing for precision.

Request body:

FieldTypeDescription
pathstringPath to the file to modify.
replacementsarrayList of find-and-replace operations (applied sequentially).

Each replacement chunk:

FieldTypeDefaultDescription
targetstring(required)Exact string to find. Must match precisely, including whitespace.
replacementstring(required)Content to replace the target with.
start_lineintegernullNarrow the search to lines at or after this (1-indexed).
end_lineintegernullNarrow the search to lines at or before this (1-indexed).
allow_multiplebooleanfalseIf true, replaces all occurrences. If false, errors when multiple matches are found.
curl -X POST http://localhost:8000/files/replace \
-H "Authorization: Bearer <api-key>" \
-H "Content-Type: application/json" \
-d '{
"path": "/home/user/config.yaml",
"replacements": [
{"target": "debug: false", "replacement": "debug: true"}
]
}'

Search File Contents

GET /files/search

Search for a text pattern across files in a directory. Returns structured matches with file paths, line numbers, and matching lines. Skips binary files.

ParameterTypeDefaultDescription
querystring(required)Text or regex pattern to search for.
pathstring.Directory or file to search in.
regexbooleanfalseTreat query as a regex pattern.
case_insensitivebooleanfalsePerform case-insensitive matching.
includestring[]nullGlob patterns to filter files (e.g. *.py). Files must match at least one pattern.
match_per_linebooleantrueIf true, return each matching line. If false, return only filenames.
max_resultsinteger50Maximum number of matches to return (1–500).
curl "http://localhost:8000/files/search?query=TODO&path=/home/user/project&include=*.py" \
-H "Authorization: Bearer <api-key>"
{
"query": "TODO",
"path": "/root",
"matches": [
{"file": "/root/app.py", "line": 42, "content": "# TODO: refactor this"},
{"file": "/root/utils.py", "line": 7, "content": "# TODO: add tests"}
],
"truncated": false
}

File Transfer

Upload a File

POST /files/upload

Save a file to the container filesystem. Provide either a url to fetch remotely, or send the file directly via multipart form data.

ParameterTypeDescription
directorystringDestination directory for the file.
urlstring(Optional) URL to download the file from. If omitted, expects a multipart file upload.

From URL:

curl -X POST "http://localhost:8000/files/upload?directory=/home/user&url=https://example.com/data.csv" \
-H "Authorization: Bearer <api-key>"

Direct upload:

curl -X POST "http://localhost:8000/files/upload?directory=/home/user" \
-H "Authorization: Bearer <api-key>" \
-F "file=@local_file.csv"

Via temporary upload link (no auth needed to upload):

# 1. Generate an upload link
curl -X POST "http://localhost:8000/files/upload/link?directory=/home/user" \
-H "Authorization: Bearer <api-key>"
# → {"url": "http://localhost:8000/files/upload/a1b2c3d4..."}

# 2. Upload to the link (no auth required)
curl -X POST "http://localhost:8000/files/upload/a1b2c3d4..." \
-F "file=@local_file.csv"

Opening a temporary upload link in a browser shows a simple file picker form — useful for manual uploads without curl.

The filename is automatically derived from the uploaded file or the URL.

Download a File

GET /files/download/link

Returns a temporary download URL for a file. The link expires after 5 minutes and requires no authentication to use.

curl "http://localhost:8000/files/download/link?path=/home/user/output.csv" \
-H "Authorization: Bearer <api-key>"
{"url": "http://localhost:8000/files/download/a1b2c3d4..."}

Process Status (Background)

GET /processes/{process_id}/status

Poll the output of a running or finished background process. Uses offset-based pagination so agents can retrieve only new output since the last poll.

Query parameters:

ParameterDefaultDescription
wait0Seconds to wait for the process to finish before returning.
offset0Number of output entries to skip. Use next_offset from the previous response.
tail(all)Return only the last N output entries. Useful to limit response size.
curl "http://localhost:8000/processes/a1b2c3d4/status?offset=0&tail=20" \
-H "Authorization: Bearer <api-key>"
{
"id": "a1b2c3d4",
"command": "make build",
"status": "running",
"exit_code": null,
"output": [{"type": "stdout", "data": "Building...\n"}],
"truncated": false,
"next_offset": 1,
"log_path": "/root/.open-terminal/logs/processes/a1b2c3d4.jsonl"
}

Health Check

GET /health

Returns service status. No authentication required.

{"status": "ok"}

Security Considerations

  • Always use Docker in production. Running Open Terminal on bare metal exposes the host system to any command the model generates.
  • Non-root by default. The container runs as a non-root user (user) with passwordless sudo available when elevated privileges are needed. This adds a layer of safety while preserving full capability.
  • Set an API key. Without one, anyone who can reach the port has full shell access. If you don't provide one, the auto-generated key is printed once at startup — save it.
  • Use resource limits. Apply --memory and --cpus flags in Docker to prevent runaway processes from consuming host resources.
  • Network isolation. Place the Open Terminal container on an internal Docker network that only Open WebUI can reach, rather than exposing it to the public internet.
  • Use named volumes for persistence. Files inside the container are lost when the container is removed. The default docker run command mounts a named volume at /home/user for persistence.
  • Log files. Process output is persisted as JSONL files under the configured log directory. Review these files periodically and apply retention policies as needed.

Further Reading