A self-extending Rust agent harness for coding in the terminal.
Open Max is a single binary that runs a focused agent loop in your project directory and streams every tool call to the terminal. Point it at the model server you choose: local, cloud, or a private proxy. No desktop shell, no heavyweight runtime, no telemetry.
The endpoint must return structured API tool_calls to use tools. For local servers, configure the model's tool parser and chat template. Call markup in assistant text is displayed as text and never executed. Responses are limited to 16 MiB and 128 tool calls.
You own the endpoints, the tools, the skills, and the context.
- Small by default. Seven built-in tools (
list_dir,read_file,write_file,edit_file,glob,grep,bash) and a short system prompt. Old tool output is dropped before your task is, and dropped context is summarized by your own model into a compact note (heuristic digest as fallback) whose address points at the lossless archive of everything dropped — compaction is a bounded view over a record you can always read back. - Your model, your server. One
base_url, or several named endpoints inproviders.jsonswitched with/model. Works with local servers (Ollama, LM Studio, vLLM, llama.cpp), cloud gateways (OpenRouter and similar), and private proxies. - Trust before execution. An exact canonical project root must be trusted before any agent turn or project behavior starts. Interactive use asks once; headless and stdio runs fail closed until a human grants trust with
--trust-project. - Newly trusted projects run in auto. Trusting a project records its approval mode,
autounless you pick another: tools and extensions run without per-call approval while deny rules, hooks, and execution reports stay active. Pickaskto havewrite_file,edit_file, andbashwait for approval, orreadonlyto block mutating tools. Approvals and permissions decide whether Open Max dispatches a tool call; they are not OS isolation. - File based extensions. Drop TOML tools,
SKILL.mdskills, prompt templates, and process hooks under project or home config. No fork required. The agent writes them itself and the harness re-freezes as soon as a mutating call lands, so a tool the agent writes is a tool the agent uses on its very next step. - File based memory. One durable fact per file in
.openmax/memory/, written by the agent, surfaced as an index line in future sessions, ranked by recency and frequency of observed use. Old entries fade from the index and remain searchable on disk until the user or agent deletes them. No database, no daemon, no embeddings; zero prompt cost when empty. - Recall over everything kept.
openmax --recall "<query>"searches this project's past sessions, compaction archives, digests, and memories in one bounded streaming pass — BM25 relevance fused with the same recency law, ranked excerpts, each citing the file with the full record. No index to build or maintain: the stores on disk stay the single source of truth. - Visible work. Reads, greps, diffs, and shell commands stream as they happen in a fullscreen TUI. Headless print mode for scripts and CI.
- Local sessions. Conversation state lives under
~/.openmax/. The harness contacts only the model endpoint you configure.
The design starts with one question: What is the smallest capability Open Max must provide so the agent can construct richer behavior itself? The answer is one focused native loop, seven primitive tools, a fast event-driven TUI, context management, and stable file and process contracts.
| Need | Construct it with |
|---|---|
| External service or specialized capability | A CLI-backed TOML tool plus an on-demand skill, without an MCP runtime |
| Reusable workflow or command | A SKILL.md package or prompt template slash command |
| Isolated or parallel work | A child openmax -p or interactive openmax --stdio process, usually in tmux |
| Durable background work | A named tmux session that the agent can inspect and reattach |
| Planning and task state | PLAN.md and TODO.md, visible to the user and every tool |
| Lifecycle policy and events | Process hooks and permission files |
| Compaction integration | The built-in model summary plus the compaction hook event |
| Model endpoints | providers.json, including local servers, gateways, and private proxies |
| Shortcuts or a completely different UI | Prompt templates, or a custom frontend speaking openmax-stdio/8 |
These are deliberate boundaries, not placeholders for hidden orchestration products. Open Max does not carry an MCP host, nested-agent scheduler, plan mode, background-job product, built-in TODO database, user-keybinding engine, pluggable compactor, or TUI plugin ABI. The agent composes those richer workflows from the same host tools a developer can inspect, edit, test, and remove.
Homebrew, on macOS or Linux:
brew install Max17190/tap/openmaxOr without Homebrew:
curl -fsSL https://useopenmax.dev/install.sh | shOr with bun or npm, if you already have a JavaScript runtime:
bun install -g openmax-cli # or: npm install -g openmax-cliThe command is openmax either way; only the package name carries the -cli,
because openmax on npm belongs to an unrelated 2015 package. This path fetches
the same native binary through a small launcher script. Up to and including
2026.10.1 that script runs under node only, so with those versions this is
the one install that needs Node.js on your PATH, even when bun did the
installing. Releases after 2026.10.1 run it under node when Node.js is on
your PATH and under bun when it is not, so either runtime is enough. Bun
blocks install scripts by default, which is fine here: the binary is fetched on
first run instead, once, and cached.
Either way there is no Rust toolchain and no build. The script picks the right
prebuilt binary for your machine, checks it against the published SHA-256, and
puts openmax in ~/.openmax/bin next to the settings and sessions the
harness already keeps there. It adds that directory to your PATH through your
shell profile, which only a new shell reads, so open one (or
source ~/.openmax/bin/env) before typing a bare openmax. The shell
installer of releases after 2026.10.1 ends with the first command to run (see
Use), naming the binary by its absolute path, with the home
directory spelled out (/Users/you/.openmax/bin/openmax, for example), so it
works in the shell that ran the install.
The check uses sha256sum, or shasum -a 256 where there is no sha256sum
(macOS 14 and earlier), and is skipped, with a message, only on a system with
neither. The installers of 2026.10.1 and earlier skip it on any system without
sha256sum.
macOS and Linux, x86_64 and arm64. Linux gets a glibc build, or a static musl build automatically when glibc is older than 2.35, so it also runs on Alpine and inside slim containers. Windows is not supported.
Set OPENMAX_NO_MODIFY_PATH=1 to leave your PATH alone, or
OPENMAX_INSTALL_DIR to install somewhere else:
curl -fsSL https://useopenmax.dev/install.sh | OPENMAX_NO_MODIFY_PATH=1 shuseopenmax.dev/install.sh redirects to the current release's installer, so it
follows new releases without the link changing.
Every release also carries the
plain tarballs and a sha256.sum, so you can download and verify by hand
instead of piping a script into a shell.
From source instead, which needs Rust 1.89 or newer:
git clone https://github.com/Max17190/open-max.git
cd open-max
cargo install --path crates/tui --lockedOr run it in place with cargo run --release -p openmax.
The first run can configure Open Max for you: pass the endpoint as flags, as
in Use, and each flag whose key ~/.openmax/settings.json does not set
yet is written there before the turn. Or edit the file yourself:
{
"base_url": "http://127.0.0.1:11434/v1",
"model": "qwen2.5-coder:7b",
"api_key": null,
"approval_mode": "ask",
"context_tokens": 32768,
"max_parallel_tools": 4
}base_url is the root of your model's HTTP API (the harness calls chat/completions on it). Set model to the id that server expects. Set api_key to a literal or $ENV_VAR, or export OPENMAX_API_KEY. Set context_tokens to the context window the server actually serves for that model; nothing is queried, and a guessed window is wrong in one direction or the other. There is no default endpoint, model, or window: until all three are configured (here or through a named provider), Open Max refuses to start a turn with an error that names each missing key and the flag that writes it.
--base-url, --model, --api-key and --context-tokens set the key of the same name. A flag whose key settings.json leaves unset (no file yet, an empty base_url or model, no api_key or context_tokens) is written to the file before the run, on a run that uses the endpoint the file selects, and the run says which keys it wrote. A key the file already sets is overridden for that run only: the file keeps its value, a /model or /provider save during the run keeps it too until the session changes that key or switches provider, and the run says which keys the command line overrode. A --base-url for another server than the file's is not sent the file's api_key. A flag equal to the file's value changes nothing. The full rule, with --provider, is in configuration.
Trusting a project records its approval mode: auto unless you pick ask or
readonly at the trust prompt. Auto covers extension creation and repair
without further confirmation; validation, deny rules, and execution reports
remain active. /approvals auto|ask|readonly, Shift+Tab, and the approval
card's Auto for project choice change the saved mode for the current trusted
project. approval_mode in settings.json applies only to a project with no
saved mode, such as one trusted by an earlier version. See
approval modes.
A settings file that exists but does not parse, uses an unknown key, or sets an unrecognized approval_mode is a startup error (fail closed): Open Max exits with the parse reason instead of silently reverting your endpoint and approval policy to defaults.
Named endpoints in providers.json and compat flags for picky gateways are covered in configuration.
The first run is one command. In your project, give it your model server's API root, model id and context window, and a first task:
cd ~/code/my-app
openmax --trust-project --base-url http://127.0.0.1:11434/v1 --model qwen2.5-coder:7b --context-tokens 32768 -p "summarize this repo" # from a terminalIt trusts the project in auto mode (a project trusted already keeps its
mode), writes the endpoint to ~/.openmax/settings.json, and prints the
answer, with no file to write by hand and nothing to restart. For a server that checks a key, add
--api-key '$NAME' naming the environment variable that holds it, exported
before you run the command: the single quotes keep your shell from expanding
it, so settings.json stores the variable's name and the key is read from the
environment when a request is sent. A literal key works too, and is stored as
given, in plain text in settings.json.
After that, openmax alone starts the fullscreen session:
cd ~/code/my-app
openmaxOn the first interactive run, inspect the project and answer the trust prompt: y trusts it in auto mode, a in ask, and r in readonly. The interface paints its first frame before it reads trust, settings or the session, then asks on your shell. Headless and stdio runs never prompt: grant trust once from a terminal with --trust-project, which records auto, and later runs, including a frontend's openmax --stdio, need no flag:
openmax --continue # resume latest session here
openmax --trust-project -p "summarize this repo" # from a terminal
openmax --stdio # a frontend, once trustedA frontend's stdin is its protocol pipe, not a terminal, so it cannot grant trust itself. For CI and other automation a human runs, see project trust.
In print mode, text goes to stdout and tool progress to stderr; with --json, each AgentEvent is one JSON line. Press / for slash commands and @ to mention a project file. Full flags, keybindings, and commands are in usage.
With nothing installed, extensions cost zero tokens. Project paths win over global ones on name collision. Each surface is a file the agent can write, openmax --check can validate, and the next turn picks up automatically.
| Surface | Location | What it is |
|---|---|---|
| Tools | .openmax/tools/*.toml |
Schema-described native processes; args on stdin, result on stdout |
| Skills | .agents/skills/*/SKILL.md |
On-demand workflow packages; only name and description cost prompt tokens |
| Prompt templates | .agents/prompts/*.md |
User-invoked messages; the file stem becomes a slash command |
| Hooks | .openmax/hooks/ |
Process gates on a fixed set of lifecycle events; three of them can block |
| Permissions | .openmax/permissions.toml |
Allow, deny, or ask rules matched against tool arguments; fail closed |
# .openmax/tools/todo_scan.toml
name = "todo_scan"
description = "List TODO/FIXME comments with file and line"
command = "sh"
args = ["./scripts/todo-scan.sh"]
timeout_secs = 30
mutating = false
[params]
type = "object"
required = ["path"]
[params.properties.path]
type = "string"
description = "Directory to scan"Unknown keys are rejected in tool, hook, and permission files and in settings.json, and openmax --check names any in providers.json, so a misspelled field there shows up in openmax --check rather than silently changing behavior (SKILL.md and template frontmatter ignore keys they do not read). openmax --spec <surface> prints the complete authoring contract for any one of them, and the frozen prompt carries only a one-line pointer to it, so that contract costs zero tokens until the agent reads it.
File formats, hook events, permission rule syntax (including the in-session repair path for a broken permissions file), self-description, and the freezing model are documented in extending.
The built-in file tools (list_dir, read_file, write_file, edit_file, glob, and grep) are confined to the project root by the harness.
write_file and edit_file write the new contents to a temp file beside the target and rename it into place, so a write that fails (a full disk, a quota) leaves the file as it was.
- The temp file that replaces a file is created with mode
0600, readable and writable only by its creator, until it has the target's owner, group and permissions. - The temp file takes the target's permissions (less setuid and setgid), owner, group and access control list. It does not take the directory's default list, or the target's other extended attributes or file flags.
- The temp file needs room for the whole new file beside the old one, so on a full disk even a write that would shrink a file is refused, and the file is left unchanged.
- A replacement is a new file under the old name, so whatever holds the old file keeps it. A mount of the file itself (a container's single-file bind mount) and another process's open descriptor or inode watch go on seeing the old contents, so mount or watch the directory instead.
A file is written in place instead, and its old contents are written back if that write fails, when:
- it has other hard links;
- Open Max cannot give a new file its owner or group (only the superuser can give a file to another user or to a group it is not in);
- its access control list cannot be given to a new file (on macOS, where Open Max does not copy one, a file that has a list, or whose directory's inheritable entries would give the new file one);
- its directory takes no new file;
- the rename over it is refused (a file mounted on its own);
- or its path leaves no room for the longer path of a temp file beside it.
Such a file whose contents cannot be read first is not written at all, since there would be nothing to write back, and the write reports it unchanged. In a directory flagged append-only, which refuses both the rename and the removal of the temp file, the file is written in place and the temp file, holding the new contents, is left beside it.
bash, external TOML tools, and hooks are native processes. The harness does not confine them to the project root. They inherit the host filesystem, environment, credentials, and network access of Open Max, except that an external tool's environment is a scrubbed baseline (PATH, HOME, LANG, TERM) plus the variables its manifest's env names. They run without a controlling terminal, so a command that prompts on the terminal (a password, an ssh host key) fails at once instead of drawing over the TUI and waiting out its timeout. bash and hooks also get GIT_TERMINAL_PROMPT=0, so git names the cause.
bash runs each command with bash -c (/bin/sh -c on a host with no bash at a fixed path), but the command text is not that argument: it reaches the shell in the OPENMAX_COMMAND environment variable, and the shell runs the fixed script eval "unset OPENMAX_COMMAND; $OPENMAX_COMMAND". So the shell's argument list never holds the text, and since eval unsets the variable before the command's first word, no process the command starts inherits it, whether or not bash starts with allexport on (as an exported SHELLOPTS=allexport or a BASH_ENV file that runs set -a arranges). A BASH_ENV file runs before that unset, so it, and anything it starts, can still read the text there. OPENMAX_COMMAND is a name the harness reserves for bash calls: a value exported in the harness's own environment is overwritten for the call and unset before the command runs. A pkill -f or pgrep -f pattern that occurs only in the command text therefore matches the processes it names and not the shell running it. $0 is the shell's path and there are no positional parameters, as under a plain bash -c.
Running the text through eval shows in a few places. Bash's set -x trace lines start with ++ instead of +. Bash's $_ is OPENMAX_COMMAND when the command starts, and once the command text has run, as an EXIT trap sees it, $_ holds unset OPENMAX_COMMAND; and the text; bash gives each child its own _, so none inherits it. Bash's BASH_EXECUTION_STRING holds the fixed script, not the text. A syntax error names eval where it named -c (bash's -c: line N: becomes eval: line N:, and dash adds eval: after its line number), and a first line that bash quotes back in the error starts with unset OPENMAX_COMMAND; . Bash 3.2 (the macOS /bin/bash) exits 1 for a syntax error where bash -c exited 2; bash 5.3 and dash exit 2 either way. A command killed by a signal can be reported as the shell's exit code, 128 plus the signal's number (139 for a segfault), instead of as the signal: under -c, bash and dash exec'd a lone simple command in the shell's place, and dash and bash 5.1 and later also the last command of a list, but none of them does inside eval. Bash's or dash's own stderr line names most signals (not SIGINT or SIGPIPE).
Each bash or external tool call runs in its own process group, and processes it leaves running in the background are terminated when it returns. The first call in a session that terminated some gets a result that explains why and how to keep one running (a named tmux session); every later one gets one line that says so on its own, which still holds if the model never saw the first (its turn was cancelled) or compaction dropped it. A session loaded again from its file, as a resume does, or one whose extensions were refrozen, explains once more.
Permissions, approvals, and mutating metadata control dispatch and user experience, not operating-system isolation.
Open Max itself does not phone home. Apart from native child processes, the harness contacts only the model endpoint you configure.
Sessions, settings, tools, and skills stay under ~/.openmax/ and your project directory. /status lists the destinations configured by the harness and detailed runtime information; it does not enforce or enumerate child-process network access, and external tools you install may open their own network connections.
- Configuration: settings, approvals, providers, project trust
- Usage: CLI flags, keybindings, slash commands
- Extending: tools, skills, templates, hooks, permissions, validation, freezing
- stdio protocol: the
openmax-stdio/8contract for custom frontends
cargo check
cargo test
scripts/check-suite-time.sh
cargo build --release -p openmaxcargo test runs the test binaries one after another. scripts/check-suite-time.sh, run once they are built, runs them all at once and fails when the suite takes longer than 5 seconds, or when a test fails (a test binary that fails beside the others is run again alone and fails the check only if it fails again); CI, whose runners have fewer cores, runs it with three times that budget on Linux and four times it on macOS. The script, and the test in crates/tui/tests/thesis.rs that drives it as part of cargo test, need bash and perl with its core modules JSON::PP and Time::HiRes (on Debian and Ubuntu, the perl package: perl-base alone has neither).
Set OPENMAX_PERF=1 while running the TUI to log frame, transcript-layout, and selection-overlay timings. Core logic is in open-max-core (crates/core/src/agent.rs); the TUI is crates/tui/src/app.rs.
Open Max is early software. File an issue or send a PR if something breaks.
MIT. See LICENSE.