An Open WebUI pipe function that runs each chat turn as a headless Claude Agent SDK session: Claude Code's full agent loop, with a real working directory and tools, behind a normal chat UI, billed to an Anthropic API key or, on a single-user install, your Claude subscription. It is for one person (or one trusted admin) who already runs Open WebUI and wants Claude Code in it, on the web and on mobile clients, rather than only in a terminal.
The agent can stop mid-turn and ask through Open WebUI's own form:
Every reply ends with what the turn cost:
This is a fork of Thomas Friedel's
openwebui-claude-code
(MIT), taken at commit 5bbc1fc. He built the bridge: the pipe, the valves,
the knowledge-base tools, inline tool details, image attachments, and the
artifact scanner are his work and still form the backbone of this file. This
repo adds durable sessions, output redaction, the status stream, ask_user,
chat search, and the rest of the changelog on top, and
maintains it now that upstream has gone quiet. If you find it useful, his
repo is where the idea came from.
- Real agent loop — Read/Write/Edit/Bash/Glob/Grep/WebSearch/WebFetch and subagents, in a per-chat working directory, streamed token by token.
- Durable sessions — each chat maps to one Claude Code session that
survives Open WebUI restarts and function redeploys (in a container, set
CLAUDE_CONFIG_DIRto a persistent path too); a cold start replays the chat history once and resumes warm from then on. - Keyless clients — OpenAI-API callers that send no chat id (mobile apps, voice) get a session too, fingerprinted from the conversation prefix.
- Live status — tool activity, a heartbeat while a tool runs, subagents
grouped under their parent, and a
Done · 1m42s · 74k/200k (37%) · 12 toolsline at the end; the message's ⓘ usage popover also gets the turn's duration and the subscription's usage windows (session, weekly, per-model, extra usage) with the time left until each resets. ask_user— the agent can pause and ask up to three multiple-choice questions through Open WebUI's own form, and get the answers in the same turn. The questions also appear in the reply, and typing an answer in the chat works when the form is not showing.- Earlier chats —
search_chats/read_chatlet the agent look up the calling user's past conversations (sqlite deployments; read-only; scoped to that user). - Knowledge bases — a Workspace Model's attached knowledge becomes
search_knowledge/list/read/greptools the agent calls itself. - Images and artifacts — attached images are written to the workdir for
the agent to read; files the agent creates in the workdir (and in
/tmp, ifSCAN_TMP_ARTIFACTSis on) come back inline or as links. - Repo-rooted chats —
#repo:<name>on a first message runs the chat inside an allowlisted repository (and loads itsCLAUDE.mdwhenSETTING_SOURCESincludesproject). - Output redaction — API keys, tokens and private keys are scrubbed from the reply stream and status events before they reach the chat database.
- Effort, budget, fallback — per-chat Reasoning Effort or per-turn
/effort <level>, a task token budget the model paces itself against, and a fallback model. - Cold-resume guard (opt-in,
COLD_RESUME_GUARD) — a message to a large chat idle past the prompt cache gets a cost warning and a pickup note for a new chat instead of a full-price context rebuild;continueor/resumegoes ahead.
- Open WebUI with the Functions framework; tested on 0.11.3.
- The Python Open WebUI runs on (3.11 or newer).
claude-agent-sdk0.2.152 or newer is installed by Open WebUI from the file'srequirements:line and bundles the Claude Code CLI, so no Node.js and no separateclaudeinstall on the host. - An Anthropic API key from the Claude Console,
or, for a single-user install, a Claude Pro/Max/Team subscription (one-time
claude setup-tokenon any machine with a browser). See Which credential before choosing. - A directory the Open WebUI process can write, for
WORKDIR_ROOT.
Tested on macOS 15 (native install) and on Linux as root in the official
ghcr.io/open-webui/open-webui:main image, with Open WebUI 0.11.3,
claude-agent-sdk 0.2.152, Claude Code CLI 2.1.259.
Five steps: a credential, the function, its valves, the toggle, a first message.
API key (recommended). Create one in the Claude Console. Billed per token. This is the path Anthropic's docs name for products built on the Agent SDK (Legal and compliance, "Authentication and credential use"), and the only one to use on any instance with more than one user.
Subscription token (single-user installs only). On any machine with a browser and Claude Code installed:
claude setup-tokenCopy the long-lived OAuth token it prints. It authenticates your own Pro/Max/Team subscription for your own use. Anthropic currently counts Agent SDK usage against subscription limits and says it is still working out how plans should cover it; its docs also say SDK-based products should use an API key, and its position on subscription use outside Claude Code has changed before. Treat this path as unsupported and subject to change. Never put a subscription token on an instance other people use: routing other people's requests through your plan is what Anthropic's terms forbid outright.
In the UI: Admin Panel → Functions → +, paste the contents of
claude_agent_pipe.py from the latest release
(the file on main may be ahead of the changelog), set the id to claude_code
and any name, Save.
Or over the admin API (bash, from the repository root):
curl -sf -X POST http://localhost:8080/api/v1/functions/create \
-H "Authorization: Bearer $OWUI_ADMIN_KEY" -H 'Content-Type: application/json' \
--data-binary @<(python3 -c 'import json;print(json.dumps({"id":"claude_code","name":"Claude Code","meta":{"description":"Claude Code agent loop"},"content":open("claude_agent_pipe.py").read()}))')Either way, Open WebUI installs claude-agent-sdk from the file's
requirements: line on save. Allow a minute. Two things can go wrong:
- The save fails with "Error creating function": the install did not succeed. Check the Open WebUI log for the pip error.
- The host runs with
OFFLINE_MODE=true: Open WebUI skips the install entirely. Install the SDK into Open WebUI's Python environment yourself (pip install 'claude-agent-sdk>=0.2.152') and save again.
Functions → Claude Code → ⚙ Valves. Two matter on first install:
ANTHROPIC_API_KEY, orCLAUDE_CODE_OAUTH_TOKENon a single-user install: the credential from step 1. If both are set the token wins and the key is unset for the agent.WORKDIR_ROOT: a directory the Open WebUI process can write. The default is/tmp/claude-agent-pipe; use a persistent path so sessions survive reboots.
In a container, also set CLAUDE_CONFIG_DIR to a persistent path (for
example a subdirectory of a bind-mounted WORKDIR_ROOT). The Claude Code
CLI keeps its session transcripts there; left at the default they live in
the container's $HOME/.claude and vanish when the image is recreated.
Chat titles, tags and follow-up suggestions come from Open WebUI's Task
Model (Admin → Settings → Interface). With none set, Open WebUI asks the
chat's own model, and the pipe answers with one short tool-less call on
TASK_MODEL (Haiku by default). A local Task Model costs nothing per chat.
Read the Security section before leaving PERMISSION_MODE at
its default. Every other valve is described in
docs/valves.md, generated from the code so it is always
current.
Over the API, valves are a JSON object posted to
POST /api/v1/functions/id/claude_code/valves/update.
Flip the toggle on the Functions page (or
POST /api/v1/functions/id/claude_code/toggle), then pick Claude Code in
the model picker. MODELS adds one picker entry per extra model id; the API
model id is claude_code.claude-code.
The status line shows Session: new chat, then tool activity, then the Done
line. A second message shows Session: resumed.
Disable and delete the function in Admin Panel → Functions, then remove
WORKDIR_ROOT (and CLAUDE_CONFIG_DIR if you set one). Nothing else is
written outside those two directories and Open WebUI's own database.
pipe() is called once per turn. It resolves the working directory
(WORKDIR_ROOT/<chat_id>, or anon-<uuid> for keyless callers, or the
#repo: target), decides whether a Claude Code session can be resumed, and
runs the turn through ClaudeSDKClient, translating the SDK's message stream
into text chunks and Open WebUI status events.
- Sessions. The chat id → session id map is persisted to
WORKDIR_ROOT/.sessions/<chat_id>.json; the in-process dict is only a cache. Keyless callers are fingerprinted from their conversation prefix intoWORKDIR_ROOT/_sessions.json(entries expire after 30 days). A dead resume id is dropped and the turn is retried cold. - Cold starts. When no session can be resumed, the prior turns are packed
into a
<conversation_history>block ahead of the prompt (last 30 messages, 24k chars), so nothing is lost; the next turn resumes warm. - Tools. Everything Claude Code has, gated by
ALLOWED_TOOLSandPERMISSION_MODE, plus in-process MCP servers forask_user, chat search, and knowledge. Those are registered withalwaysLoadbecause Claude Code 2.1 otherwise hides MCP tools behindToolSearchand the model never finds them. - The agent's environment. The chat id is exported to the agent's
subprocess as
HUB_CHAT_ID(only when one exists), so tooling the agent runs can find out which chat it is serving. Nothing in this repo reads it. - Redaction. Every chunk and event passes through
_redact_secretsbefore leavingpipe(). A hit is logged by kind, never by value.
Read this before exposing the function to anyone but yourself.
bypassPermissionsis the default. Every user who can pick the model runs arbitrary code as the Open WebUI process user, on the Open WebUI host, with no prompts, using your credential. Treat the function as a shell for everyone it is enabled for. Restrict it to admins in Open WebUI's model access controls, or tightenPERMISSION_MODEandALLOWED_TOOLS.- Valves are global. One token, one
WORKDIR_ROOT, oneREPO_MAPfor every user of the function. Two users cannot read each other's chats (chat search is scoped by user id) but they share the filesystem and the credential. This is a single-user or single-admin design, and with a subscription token it must be single-user: see step 1 of the install. REPO_MAPhands out paths. Anyone who can start a#repo:chat runs the agent inside that repository. It grants nothingbypassPermissionsdid not already reach; it only sets the working directory.SETTING_SOURCESloads the host user's~/.claude(withuser), which can define hooks that execute code. Leave it empty unless you control the host account; see Persistent context.- What the redactor catches: prefix-shaped credentials (Anthropic,
OpenAI, Slack, GitHub, Google, AWS, 1Password, JWTs) and PEM private keys,
in the reply and in status events. What it cannot catch: bare UUID
tokens, passwords, hostnames, or anything that looks like prose. The
agent can still
cata secret into a file the chat never sees. Keep credentials out ofWORKDIR_ROOTand the mapped repos, or give the agent a hook (Claude Code'sPreToolUse) that refuses to publish them. - Running as root (the official Docker image does) makes the CLI refuse
bypassPermissionsunlessIS_SANDBOX=1; the pipe sets it. That is the CLI's own sandbox flag, not an actual sandbox.
Report a vulnerability as described in SECURITY.md.
By default the pipe passes setting_sources=[] to the SDK: each chat starts
from a clean baseline and inherits nothing from the host user's ~/.claude/
or the workdir's .claude/. That is the safe default for anything shared.
On a single-user instance, standing instructions in ~/.claude/CLAUDE.md
(a host inventory, house rules) can be loaded into every chat with the
SETTING_SOURCES valve:
| Value | Loads |
|---|---|
| (empty) | Nothing; isolated baseline (default). |
user |
~/.claude/CLAUDE.md and ~/.claude/settings.json. |
project |
The working directory's CLAUDE.md and .claude/settings.json; what makes #repo: chats pick up a repository's own instructions. |
user,project,local |
Both of the above plus .claude/settings.local.json. |
Unknown tokens are dropped. There is no way to load a CLAUDE.md without
also loading the settings.json next to it: that coupling is Claude Code's,
not the pipe's, and settings.json can define hooks that run shell commands,
permission grants, environment variables and MCP servers, for every chat,
as the Open WebUI process user, under bypassPermissions. With
CLAUDE_CONFIG_DIR set, user reads from that directory instead of
~/.claude.
The pipe imports a few Open WebUI internals. Each import is wrapped so a version that moved them degrades a feature instead of failing the turn:
| Import | Used for | If missing |
|---|---|---|
open_webui.env.DATA_DIR |
locating webui.db for chat search |
chat search reports itself unavailable (or set CHAT_DB_PATH) |
open_webui.models.chats.Chats.upsert_message_to_chat_by_id_and_message_id |
the ⓘ usage popover | no popover; the status line still shows context |
open_webui.models.files, open_webui.storage.provider.Storage |
artifact upload | files the agent creates are not linked |
open_webui.models.knowledge, open_webui.models.users, open_webui.retrieval.utils.query_collection, open_webui.main.app |
knowledge tools | the tools answer "unavailable" |
Chat search reads the sqlite chat table directly and is off by construction
on Postgres deployments (DATABASE_URL starting with postgres makes the
tools report unavailable).
Claude Code CLI coupling: alwaysLoad on SDK MCP servers (2.1.x), the
subagent tool being named Task or Agent (both handled), and ToolSearch
deferral. test_turn.py pins the name cases.
src/*.py— the source, one module per concern, concatenated in filename order into the built file. Edit these, never the built file.claude_agent_pipe.py— the built function: what you install and what the redaction consumers slice.python3 build.pyregenerates it (anddocs/valves.md);python3 build.py --checkfails CI when either is stale.redact_stdin.py— a CLI over the pipe's redactor for job workers that deliver agent output outside the chat stream (--known-envscrubs live values by value, not just by shape). Optional; the pipe does not need it.test_*.py— standalone suites, stdlib only:python3 test_sessions.py, optionally against a deployed copy (python3 test_sessions.py <pipe.py>).
See CONTRIBUTING.md for the development loop and CHANGELOG.md for what changed on top of upstream.
Merging here deploys nothing: Open WebUI runs the copy stored in webui.db.
Paste the new claude_agent_pipe.py over the function in Admin → Functions,
or post it to POST /api/v1/functions/id/claude_code/update with the same
JSON shape as the create call. Valve state lives in webui.db and survives
redeploys. The author's own deploy wrapper (backup, post, verify parity, run
the suites against the deployed copy) lives in a separate homelab repo and is
not needed to use this one.
MIT, see LICENSE. Copyright is shared with the upstream author, as the file says.

