What is in open-computer-use's system prompt?
open-computer-use's full system prompt: 1 version, 10,076 characters. Audited against AISPA.
The full text of 1
prompt is reproduced below,
10,076 characters in all, each read
instruction by instruction against the eight
AISPA dimensions.
Nothing was flagged as working against the person on
the other end.
10076 characters
# System Prompt Delivery — Six MCP-Native Tiers
The Computer Use Server delivers the same per-session system prompt through **six different channels**, so clients with varying MCP support all get it. Every tier renders from the same source (`computer-use-server/system_prompt.py::render_system_prompt`) with a shared 60-second in-process cache, so fan-out cost is one render per `(chat_id, user_email)` per minute.
**Redundancy is by design.** A client might strip `InitializeResult.instructions` and never call `resources/list` — but it will always call `tools/list`, and the tool descriptions nudge the model toward `/home/assistant/README.md` inside the sandbox. That file is always present.
**Why not `@mcp.prompt("system")`?** MCP `prompts/*` is user-controlled (slash-commands the user explicitly picks) and `PromptMessage.role` is restricted to `{user, assistant}` — naming a prompt `"system"` both clashes with the spec's role model and duplicates `InitializeResult.instructions`, which is the canonical field for "how the server wants itself used." We chose the canonical path.
## The Tiers
| # | Surface | Where it lives | Who uses it |
|---|---|---|---|
| 1 | Tool descriptions | `tools/list` — `bash_tool` + `view` docstrings mention README.md | Every MCP client (tools are mandatory) |
| 2 | `/home/assistant/README.md` | Rendered into the sandbox on container creation via `put_archive` | Any model that runs the `view` tool |
| 3 | Static `instructions=` hint | FastMCP constructor, one-line pointer to README + `resources/list` | Claude Desktop, MCP Inspector; Agents SDK exposes via `server.server_initialize_result` |
| 4 | Dynamic `InitializeResult.instructions` | Per-request ContextVar, swapped onto `mcp._mcp_server` as a `@property` | Same clients as #3, with chat-specific content |
| 5 | `resources/list` + `resources/read` | Uploaded files surfaced as `FunctionResource` per chat, URI `file://uploads/{chat_id}/{rel_path}` | Agents SDK, Inspector, Claude Desktop |
| 6 | `GET /system-prompt` HTTP | Backward-compat endpoint with header > query priority | Open WebUI filter; external integrations (n8n) |
## Tier 1 — Tool description nudges
Docstrings of `bash_tool` and `view` tools (in `computer-use-server/mcp_tools.py`) end with:
> If you've lost track of your environment (chat_id, file URLs, available skills), re-read /home/assistant/README.md.
Deliberately **not** "read this first" — the system prompt itself (Tiers 3/4) already identifies as "contents of /home/assistant/README.md", so if the client surfaced it the model already has the content. This line is a **recovery hint**, not a forcing function.
## Tier 2 — README.md in the sandbox
When `docker_manager._create_container` spins up a chat's workspace container, it calls `render_system_prompt_sync(chat_id, user_email)` and writes the result to `/home/assistant/README.md` (or `/root/README.md` for the test image) via `container.put_archive`. The file survives across container removals because it lives in the chat's persistent workspace volume (`chat-{chat_id}-workspace`).
Does **not** enumerate uploaded files — those are Tier 5's responsibility and are refreshed on every upload. README is static-per-container and changes only when `user_email` changes (which doesn't happen mid-chat).
## Tier 3 — Static `instructions=`
FastMCP's constructor kwarg. A one-liner pointing at Tiers 2 and 5 (README + uploaded-file resources) so a client that renders only `InitializeResult.instructions` still learns where the per-session content lives.
## Tier 4 — Dynamic `InitializeResult.instructions`
The same `instructions` field, but **per-request**. Relies on three facts pinned in the SDK source (`.venv/lib/python3.13/site-packages/mcp/server/...`):
1. `streamable_http_manager._handle_stateless_request:196` calls `self.app.create_initialization_options()` inside a per-request task spun up for each HTTP hit — and we run `stateless_http=True` (`mcp_tools.py:276`).
2. `lowlevel/server.py:188` reads `self.instructions` at that moment to populate `InitializationOptions`.
3. `session.py:183` echoes it into `InitializeResult.instructions`.
Mechanism:
- `MCPContextMiddleware` (runs before every MCP request) pre-renders the prompt via `render_system_prompt(...)` and stores it in `current_instructions: ContextVar[str]`.
- `_DynamicInstructionsServer` subclasses `mcp.server.lowlevel.Server` with `@property def instructions` returning the ContextVar value (falling back to the static `_STATIC_INSTRUCTIONS` string when unset).
- After `FastMCP(...)` constructs the lowlevel server, we **swap the class** on the existing instance: `mcp._mcp_server.__class__ = _DynamicInstructionsServer`. No reconstruction needed.
**Stateful mode would break this** (a long-lived session caches `init_options` at construction). Do NOT flip `stateless_http=False` without re-reading the SDK source above.
**Private-API caveat.** We touch `mcp._mcp_server` and `_resource_manager._resources`. Pin `mcp` narrowly in `computer-use-server/requirements.txt` — an SDK minor bump requires re-verifying these attribute shapes.
## Tier 5 — Uploaded files as MCP resources
`resources/list` returns a `FunctionResource` per uploaded file with URI `file://uploads/{chat_id}/{url-encoded rel_path}`. `resources/read` fetches the content — text for `text/*` and a short MIME allowlist, base64 blob otherwise.
Why `chat_id` embedded in the URI: Agents SDK and Inspector don't re-send `X-Chat-Id` on per-resource calls, so URIs must be self-contained.
Why URL-encoding: FastMCP's `ResourceTemplate.matches` (verified at `.venv/.../templates.py:88`) uses `[^/]+` per template param — it blocks nested paths. Flattening via `urllib.parse.quote` sidesteps the limitation cleanly without forking the SDK.
Dynamic registration: `sync_chat_resources(chat_id)` clears previously-registered entries for that chat, re-adds from the current filesystem state, under an `asyncio.Lock` to avoid "dict changed size during iteration" when a concurrent `resources/list` runs during an upload. Called from:
- `docker_manager._create_container` — initial sync when the container spins up.
- `app.py:upload_file` — after `POST /api/uploads/{chat_id}/{filename}` saves a new file.
Upload itself stays on HTTP — **MCP has no upload primitive.** Community consensus is out-of-band HTTP alongside the MCP server.
## Tier 6 — HTTP `/system-prompt`
Kept for the Open WebUI filter (`openwebui/functions/computer_link_filter.py:224–363`) which fetches the prompt server-side and injects it into the LLM's system message. The endpoint reads:
```
X-Chat-Id | X-OpenWebUI-Chat-Id > ?chat_id= > "default"
X-User-Email | X-OpenWebUI-User-Email > ?user_email= > None
```
Header-priority rule consistent with the rest of the server (MCP middleware reads the same headers and aliases). Response header `X-Public-Base-URL` is still emitted so the filter's `outlet()` can build browser-facing archive/preview URLs from the server-owned `PUBLIC_BASE_URL`.
## Render cache
`render_system_prompt(chat_id, user_email)` is cache-backed with a 60-second TTL (`_RENDER_TTL_SECONDS` in `system_prompt.py`). Matches `skill_manager`'s own memory-cache TTL. Middleware runs the render on **every** MCP request to pre-fill the ContextVar for Tier 4, so the cache is load-bearing — without it, every `tools/call` would re-hit the skills provider. The second request for the same `(chat_id, user_email)` is a dict lookup.
Invalidation: `invalidate_render_cache()` (no arg → clear all; `chat_id` arg → clear that chat). Used in tests; also callable when skills change upstream.
## Duplication analysis (honest per-scenario breakdown)
**Open WebUI via LiteLLM (main scenario):**
- Filter `inlet()` fetches Tier 6 and puts the prompt into `body["messages"]` as a system message → model sees it **once**.
- Tier 4 `InitializeResult.instructions` is returned on MCP `initialize` — but LiteLLM is a tool-call proxy, it does NOT ingest `instructions` and forward it to the LLM. Tier 4 simply doesn't reach the model here.
- Tier 2 README.md sits in the container but the model only reads it if it actively calls `view`.
- The **only** real duplication source in this scenario is the recovery-nudge in `bash_tool`/`view` docstrings (Tier 1): "If you've lost track of your environment, re-read /home/assistant/README.md." A model following that hint adds a **second copy** of the same ~3–5K tokens.
- **Total: up to 2 copies** (filter inject + optional nudged `view`).
**Agents SDK / MCP Inspector / Claude Desktop (MCP-native scenario):**
- No Open WebUI filter.
- Integrator surfaces Tier 4 via `server.server_initialize_result.instructions` (Agents SDK), or Claude Desktop auto-applies it → model sees prompt **once**.
- Same story: if the Tier 1 nudge is honored, `view /home/assistant/README.md` adds a **second copy**.
- **Total: up to 2 copies**.
**Why keep the nudge.** Without it, a client that strips the system prompt leaves the model in a sandbox with zero context. The Tier 1 recovery hint is the only fallback that works in that pathological case — its token cost is paid only when the model actually needs it (context thins out, hint gets re-attended).
Follow-up options if duplication proves costly in practice:
- Weaken the nudge to "If your system prompt does not already describe this sandbox, read /home/assistant/README.md" — model self-selects.
- Drop the nudge entirely and rely on Tier 4 + filter inject; README stays as a file-only fallback.
- Teach the Open WebUI filter to skip its inject when the MCP tool is attached — only makes sense after Tier 4 reliably reaches the model through LiteLLM (today it does not).
## See also
- `docs/MCP.md` — protocol-level MCP server documentation.
- `docs/openwebui-filter.md` — how the filter consumes Tier 6.
- `computer-use-server/system_prompt.py` — the render function + cache.
- `tests/orchestrator/test_{render_cache,dynamic_instructions,mcp_resources,tool_descriptions,readme_in_container,system_prompt_endpoint}.py` — pinning tests for every tier.
Questions about open-computer-use's system prompt
Does open-computer-use's system prompt contain instructions that work against the user?
No. Nothing in open-computer-use's system prompt was flagged as working against the person the product is talking to. That is a clean result across all eight AISPA dimensions, not an absence of checking — the full text was read instruction by instruction.
How long is open-computer-use's system prompt?
10,076 characters across 1 prompt on this page. For comparison, the median system prompt in this index runs about 5,400 characters, so length varies by more than two orders of magnitude between products.
How many versions of open-computer-use's system prompt are on record?
1. Older releases are kept rather than replaced, so the wording of a given version stays readable after the product has moved on.
Where did this open-computer-use system prompt come from?
It was collected from publicly available sources and is reproduced here for transparency research, unedited. This site does not extract prompts from products itself.
How was open-computer-use's system prompt audited?
Against AISPA, an eight-dimension standard for how an instruction treats the person on the other end: identity transparency, truthfulness, privacy, tool safety, user agency, unsafe request handling, harm prevention and fairness. This audit was ai audit. The method is described in the paper behind the standard.