feat(pipe): add SETTING_SOURCES valve to opt into CLAUDE.md loading
The SDK pipe hardcoded setting_sources=[], so chats never loaded ~/.claude/CLAUDE.md or any settings.json. Expose it as a valve (default empty = isolated baseline) so single-user/homelab instances can opt into persistent context, while shared deployments keep the safe default. Parse via _parse_setting_sources(): comma-split, lowercase, drop unknown tokens so a typo can't silently widen inheritance. README documents the opt-in plus the hooks/permissions security tradeoff (CLAUDE.md and settings.json load together — a Claude Code coupling) and why the valve does not apply to the sandboxed pipe. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e100c801c4
commit
5bbc1fcdf9
51
README.md
51
README.md
|
|
@ -10,7 +10,7 @@ This is an Open WebUI **Pipe** that exposes Claude Code as a selectable model. E
|
|||
- **Per-chat workspaces** — each `chat_id` gets a sandboxed working directory that persists across turns
|
||||
- **Dual auth** — bring your own Anthropic **API key** (pay-per-token) *or* a **Claude Pro/Max OAuth token** (bills against your subscription)
|
||||
- **Streaming UI** — tool calls render inline with previews; generated images/PDFs/CSVs surface as artifacts in the chat
|
||||
- **Configurable valves** — model, permission mode, tool allowlist, max turns, workspace root
|
||||
- **Configurable valves** — model, permission mode, tool allowlist, max turns, workspace root, setting sources (`CLAUDE.md`)
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
@ -41,6 +41,55 @@ This is an Open WebUI **Pipe** that exposes Claude Code as a selectable model. E
|
|||
| `ALLOWED_TOOLS` | `Read,Write,Edit,Bash,Glob,Grep,WebSearch,WebFetch` | Comma-separated tools auto-approved without prompting. |
|
||||
| `WORKDIR_ROOT` | `/tmp/claude-agent-pipe` | Root directory for per-chat workspaces. |
|
||||
| `MAX_TURNS` | `30` | Max agent turns per user message. `0` disables the cap. |
|
||||
| `SETTING_SOURCES` | *(empty)* | Comma-separated filesystem setting sources to load: `user`, `project`, `local`. Empty = none (isolated baseline). See below. |
|
||||
|
||||
## Persistent context via `CLAUDE.md` (`SETTING_SOURCES`)
|
||||
|
||||
By default the pipe passes `setting_sources=[]` to the SDK, so **no** filesystem
|
||||
settings are loaded: each chat starts from a clean baseline and does **not**
|
||||
inherit the backend user's `~/.claude/` or the workdir's `.claude/`. This is the
|
||||
safe default for shared deployments.
|
||||
|
||||
If you run a single-user/homelab instance and want persistent environmental
|
||||
context (e.g. a host inventory or standing instructions in
|
||||
`~/.claude/CLAUDE.md`) without re-explaining it every chat, set the valve:
|
||||
|
||||
| Value | Loads |
|
||||
| --- | --- |
|
||||
| *(empty)* | Nothing — isolated baseline (default). |
|
||||
| `user` | `~/.claude/CLAUDE.md` **and** `~/.claude/settings.json`. |
|
||||
| `user,project,local` | Above plus the workdir's `.claude/settings.json` and `.claude/settings.local.json`. |
|
||||
|
||||
Each token maps to one source — `user` → `~/.claude/`, `project` →
|
||||
`<workdir>/.claude/settings.json`, `local` → `<workdir>/.claude/settings.local.json`.
|
||||
Unknown tokens are dropped.
|
||||
|
||||
> [!WARNING]
|
||||
> **Settings sources load more than `CLAUDE.md`.** A loaded `settings.json` can
|
||||
> define **hooks that execute shell commands**, permission grants, env vars, and
|
||||
> MCP servers — for *every chat*, under the backend user's identity, with the
|
||||
> pipe's default `bypassPermissions` mode. Only enable `SETTING_SOURCES` on an
|
||||
> instance you fully trust and control. **Do not enable it on multi-user or
|
||||
> public deployments** — it breaks per-chat isolation and lets host config
|
||||
> influence (or run code in) every user's session. There is no way to load
|
||||
> `CLAUDE.md` *without* also loading `settings.json` from the same source; that
|
||||
> coupling is in Claude Code, not this pipe.
|
||||
|
||||
### Does this apply to the sandboxed pipe?
|
||||
|
||||
Not directly. [`claude_agent_pipe_sandboxed.py`](./claude_agent_pipe_sandboxed.py)
|
||||
doesn't use `setting_sources` at all — it shells the `claude` CLI inside an
|
||||
open-terminal sandbox with a **per-chat `CLAUDE_CONFIG_DIR`** that's created
|
||||
fresh each chat, so there's no host `~/.claude/` to inherit and nothing to
|
||||
disable. Persistent context there is meant to come from mechanisms already built
|
||||
for isolation:
|
||||
|
||||
- **Workspace Model system prompt** — appended on every turn (`--append-system-prompt`); the natural place for standing instructions.
|
||||
- **Baking into the image** — skills are already vendored into the sandbox image at build time; a `CLAUDE.md` or settings can be baked the same way (under the per-chat `CLAUDE_CONFIG_DIR` layout) if you want file-based context.
|
||||
|
||||
Because the sandbox isolates the agent and a proxy holds the credentials, the
|
||||
security tradeoff is far milder there — but the `setting_sources` valve itself
|
||||
has nothing to act on, so it's intentionally **not** added to the sandboxed pipe.
|
||||
|
||||
## Auth notes
|
||||
|
||||
|
|
|
|||
|
|
@ -881,6 +881,22 @@ def _strip_mode_prefix(prompt: str) -> str:
|
|||
return prompt
|
||||
|
||||
|
||||
_VALID_SETTING_SOURCES = ("user", "project", "local")
|
||||
|
||||
|
||||
def _parse_setting_sources(raw: str) -> List[str]:
|
||||
"""Parse the SETTING_SOURCES valve into a list the SDK accepts.
|
||||
|
||||
Empty / unset → `[]` (no filesystem settings loaded; clean baseline).
|
||||
Unknown tokens are dropped so a typo can't silently widen inheritance.
|
||||
"""
|
||||
return [
|
||||
tok
|
||||
for tok in (t.strip().lower() for t in raw.split(","))
|
||||
if tok in _VALID_SETTING_SOURCES
|
||||
]
|
||||
|
||||
|
||||
def _needs_agent(prompt: str, files: Optional[List[Any]]) -> bool:
|
||||
"""Route-per-turn heuristic. `/agent` / `/fast` prefixes are explicit
|
||||
overrides. Attachments force agent mode (the model should be able to
|
||||
|
|
@ -957,6 +973,20 @@ class Pipe:
|
|||
default=30,
|
||||
description="Maximum agent turns per user message. 0 disables the cap.",
|
||||
)
|
||||
SETTING_SOURCES: str = Field(
|
||||
default="",
|
||||
description=(
|
||||
"Comma-separated Claude Code setting sources to load: any of "
|
||||
'"user", "project", "local". Empty (default) loads NONE — each '
|
||||
"chat runs from a clean baseline and does NOT inherit the "
|
||||
"backend user's ~/.claude/ or the workdir's .claude/ config. "
|
||||
'Add "user" to load ~/.claude/CLAUDE.md and ~/.claude/'
|
||||
"settings.json (persistent context for single-user/homelab "
|
||||
"setups). WARNING: settings.json can define hooks that execute "
|
||||
"code and permission grants — only enable on instances you "
|
||||
"trust and control. Avoid on multi-user/public deployments."
|
||||
),
|
||||
)
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.valves = self.Valves()
|
||||
|
|
@ -1239,7 +1269,7 @@ class Pipe:
|
|||
"model": self.valves.MODEL,
|
||||
"permission_mode": self.valves.PERMISSION_MODE,
|
||||
"allowed_tools": kb_tool_names,
|
||||
"setting_sources": [],
|
||||
"setting_sources": _parse_setting_sources(self.valves.SETTING_SOURCES),
|
||||
"system_prompt": system_text,
|
||||
"include_partial_messages": True,
|
||||
}
|
||||
|
|
@ -1392,9 +1422,11 @@ class Pipe:
|
|||
"model": self.valves.MODEL,
|
||||
"permission_mode": self.valves.PERMISSION_MODE,
|
||||
"allowed_tools": allowed_tools,
|
||||
# Don't load the host's ~/.claude/ or project .claude/ — OpenWebUI chats
|
||||
# should run with an empty baseline, not inherit the backend user's config.
|
||||
"setting_sources": [],
|
||||
# Which filesystem settings to load (user ~/.claude/, project .claude/,
|
||||
# local). Default empty = clean baseline, no inheritance of the backend
|
||||
# user's config. Opt in via the SETTING_SOURCES valve (e.g. "user" to
|
||||
# load ~/.claude/CLAUDE.md). See the valve's security warning.
|
||||
"setting_sources": _parse_setting_sources(self.valves.SETTING_SOURCES),
|
||||
# Stream token-level deltas so long answers type out instead of
|
||||
# appearing as one chunk when the block finishes.
|
||||
"include_partial_messages": True,
|
||||
|
|
|
|||
Loading…
Reference in New Issue