WebUI wrapper for Claude Code with auto journal - PWA frontend, REST + WebSocket API
Project description
pAInapple Code
A WebUI wrapper for Claude Code. A Python bridge that runs the CLI as a subprocess and serves a vanilla-JS PWA on top, over a WebSocket chat stream and a REST API — use Claude Code from any browser, with an auto-journal of every turn.
The desktop browser is the primary target, but the UI is mobile-friendly enough to be installable as a PWA on iOS/Android devices. Roughly a third of this app was developed on iOS and the rest on iPad with hardware keyboard.
Cautions - read first
Permissions
This is an MVP, but by default it now drives Claude through the Claude Agent SDK, so approval prompts surface as interactive approve/deny cards rather than being auto-denied. The default mode on the SDK engine is Ask — every tool call pauses for your decision (see Interactive permissions below).
If you switch to the classic line-protocol provider (--default-provider claude), the bridge runs Claude in headless mode (-p) where nothing can answer an approval prompt, so any ask is auto-denied. On that provider the best experience is 'Auto' or 'Bypass Permissions' (YOLO) modes.
Permission modes available (configurable per session in the settings popup):
- Plan — read-only
- Ask (SDK default) — every tool call pauses on an approve/deny card (Agent SDK provider only)
- Don't Ask — auto-deny unless pre-approved
- Accept-Edits — auto-allow writes + some basic bash commands like
cpormv. - Auto — Claude's AI classifier gates each tool call
- YOLO —
bypassPermissions, full access
Mode and model changes apply live to the running session on the Agent SDK provider — no restart needed.
For more info, check Claude Code docs https://code.claude.com/docs/en/permission-modes#available-modes
Terminal
The terminal widget is a real PTY running as the user that started the server. That means any potential prompt injection and other attacks (including infected npm packages) can run arbitrary commands as user. This issue isn't unique for this project, but it always worth to remind potential consequences of running not isolated AI agents that can fetch and run random code from the internet (including npm, pip or even this project).
Isolation
Thats why you should always run claude environment (especially YOLO oriented) in isolation (Docker, VM, LXC, BSD jails and other).
The Docker setup below with painapple-docker.sh wrapper is the easiest path for simple isolation.
If those concepts are unfamiliar, this MVP is probably not for you yet.
By default the server binds 127.0.0.1 over plain HTTP. Bind it to a non-loopback host and it auto-enables TLS with a self-signed cert (browsers show a one-time cert warning — see --tls). Either way, the built-in auth is a single-password gate, not a substitute for proper network controls: for real public exposure put it behind your own reverse proxy.
What it is, and what it isn't
It is a thin wrapper around Claude Code — every prompt is streamed to the CLI via --input-format stream-json --output-format stream-json. Sessions you create here can be resumed in the regular CLI with claude --resume <id>.
It is not an AI agent of its own. It does not modify your prompts, inject planning steps, parallelize work, or change Claude's behavior. The one exception is the optional shadow-git-helper agent, which knows how to query the Shadow Git history.
It is not easy installable development from anywhere on mobile. Yet. It is possible to work from your mobile as PWA app, but you need to wire-up it via network. By default server listens on 127.0.0.1, you can set it with arg --host x.x.x.x or using painapple-code docker setup (or painapple-docker.sh setup) for docker instances.
With default auto tls mode, it will use self signed cert for non-localhost interfaces which may be the issue for mobile browsers.
My advice is to keep listening on 127.0.0.1 and use reverse proxy (Caddy for example) for SSL on trusted network interface (like home or personal VPN).
Requirements
- Python 3.12+
- Claude Code CLI installed and authenticated
- Network access between client and server (any modern browser works as a client)
Quick start (Docker / Podman — recommended)
The image bundles Python 3.13, Node 20, @anthropic-ai/claude-code and some basic dev tools.
State persists in named volumes; your project and an isolated Claude CLI home are bind-mounted from the host.
Option A — built-in docker CLI (pipx, no clone needed)
The pip package ships a painapple-code docker command group with an interactive TUI setup — arrow-key menus, a browsable directory picker (type to filter, ←/→ to climb/enter folders), back navigation on every step, and a final review screen that jumps back into any section.
pipx install painapple-code
painapple-code docker # first run opens the setup wizard
painapple-code docker pull # fetch wrotek/painapple-code:latest (pull v1.0.0 to pin)
painapple-code docker up -d # start the bridge detached
In a hurry? painapple-code docker quick skips setup entirely and starts the bridge against the current directory with sane defaults.
Other subcommands: stop, down, restart, logs, shell, claude-login, password (reveal the login URL), extract, status, config (non-interactive --flag value overrides). Run painapple-code docker help for the full list. The setup wizard configures an isolated .claude home by default (the container doesn't share state with your host CLI) and offers to seed it from your host login once.
Docker and Podman are auto-detected; override with RUNTIME=docker or RUNTIME=podman if both are installed. This CLI is pull-only — building the image from source needs a repo checkout (Option C below; both tools share the same config, so mixing them works).
Option B — raw docker run (one-liner, no clone needed)
docker run -d --name painapple-code \
-p 127.0.0.1:8765:8765 \
-v "$PWD:/workspace" \
-v "$HOME/.painapple-code/.claude:/home/app/.claude" \
-v painapple-data:/data \
wrotek/painapple-code:latest
docker logs painapple-code 2>&1 | grep -E 'http(s)?://' | head -1 # bootstrap URL
Tags: :latest (newest stable), :vX.Y.Z (pinned to a specific release), :edge (manual builds off main).
Option C — build from source with ./painapple-docker.sh (clone the repo)
The Bash wrapper does everything Option A does and adds local image builds (including personalized builds layered from your devcontainer.json or Dockerfile).
./painapple-docker.sh setup # interactive: workspace, claude credentials, port, accent
./painapple-docker.sh build # build the image locally (~2-3 min, once)
./painapple-docker.sh up -d # start the bridge detached
Open http://localhost:8765/ in a browser. The first run logs a bootstrap URL with the auth password embedded as ?tkn=… — open that link once and the cookie will keep you logged in.
Manual Docker Compose (no wrapper)
docker compose build --build-arg USER_UID=$(id -u) --build-arg USER_GID=$(id -g)
WORKSPACE=/absolute/path/to/your/project docker compose up
By default the compose file mounts ~/.painapple-code/claude-home/ as the container's .claude. To seed it with your existing login:
mkdir -p ~/.painapple-code/claude-home
cp ~/.claude/.credentials.json ~/.painapple-code/claude-home/
Manual Podman (no wrapper)
podman build --build-arg USER_UID=$(id -u) -t painapple-code:latest .
podman run --rm -it --userns=keep-id \
-p 8765:8765 \
-v painapple-data:/data \
-v "$HOME/.painapple-code/claude-home:/home/app/.claude:Z" \
-v "/absolute/path/to/your/project:/workspace:Z" \
painapple-code:latest
Quick start (GitHub Codespaces / Dev Containers)
Drop one line into a project's .devcontainer/devcontainer.json and every Codespace you open on that project boots with pAInapple Code installed, started, and port-forwarded — ready to connect from an iPad or any browser:
{
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
"features": {
"ghcr.io/wrotek/painapple-code/painapple-code:1": {}
},
"forwardPorts": [8765]
}
Set ANTHROPIC_API_KEY as a Codespaces secret (or claude login in the codespace shell once). See examples/devcontainer/ for a complete config and features/src/painapple-code/ for Feature options.
Want to drop pAInapple Code into an already-running Codespace without
touching devcontainer.json (e.g. to share with a tester)? Run the
Feature's install.sh directly:
curl -fsSL https://raw.githubusercontent.com/wrotek/painapple-code/main/features/src/painapple-code/install.sh \
| sudo bash
Note: this won't survive a "Rebuild Container". For persistent installs use the features block above.
Quick start (run directly)
No container — the bridge runs bare on the host (needs the Claude Code CLI installed and authenticated):
pipx install painapple-code
painapple-code --workspace /path/to/your/project
Or from a clone:
git clone <repo-url> && cd painapple-code
python3 -m venv venv
venv/bin/pip install -e .
venv/bin/python -m painapple_code --workspace /path/to/your/project
(source venv/bin/activate is bash-only. On fish/csh use venv/bin/python directly, or your shell's matching activate script.)
Open http://localhost:8765/ and follow the bootstrap URL printed on first run.
Authentication
The server requires a password for every HTTP and WebSocket request. On first start it generates a token and stores it in the config dir (~/.config/painapple-code/config.yaml — inside the container image that's under /home/app/; mode 0600 either way), then logs a bootstrap URL with the token embedded as ?tkn=….
# Reveal the password
awk '/^password:/ {print $2}' ~/.config/painapple-code/config.yaml
# …or when running in a container
docker exec painapple-code awk '/^password:/ {print $2}' \
/home/app/.config/painapple-code/config.yaml
# Rotate (delete → restart, new password generated on next start)
rm ~/.config/painapple-code/config.yaml # then restart the server
# …or when running in a container
docker exec painapple-code rm /home/app/.config/painapple-code/config.yaml
docker restart painapple-code
docker logs painapple-code 2>&1 | grep -E 'http(s)?://' | head -1
Three auth paths work for HTTP requests:
- Cookie
bridge_auth=<HMAC-derived-token>(set automatically after first auth) - Query string
?tkn=<password>(HTTP middleware also issues aSet-Cookie) - Header
Authorization: Bearer <password>(forcurl/ scripts)
Server options
These flags apply to the default server invocation (painapple-code [flags], or the explicit painapple-code serve [flags]). Container lifecycle lives under the painapple-code docker … subcommand group — see Quick start above.
| Flag | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
Bind address |
--port |
8765 |
Listen port |
--workspace |
. |
Workspace directory — the dir Claude operates in. (--cwd is an alias.) |
--workspace-root |
--workspace |
Directory scanned for sibling project folders surfaced on the welcome screen |
--instance-name |
— | Label for PWA icon and header (e.g., DEV, STABLE) |
--accent |
— | Accent color: preset name or hex (#f87171) |
--tls |
auto |
TLS mode: auto (on for non-loopback binds), on, off. Self-signed cert auto-generated; fingerprint shown at startup |
--tls-cert |
auto-generated | TLS cert path (default <config-dir>/cert.pem) |
--tls-key |
auto-generated | TLS key path (default <config-dir>/key.pem) |
--tls-fp-url |
off | Append the cert fingerprint to login URLs as &fp=… (for cert-pinning clients; browsers ignore it) |
--shadow-db |
~/.painapple-code/shadow.duckdb |
DuckDB path for the turn store |
--log-dir |
~/.painapple-code/logs/ |
Log directory |
--auth-config-file |
~/.config/painapple-code/config.yaml |
Override auth config path |
--default-provider |
claude-sdk |
Engine for new sessions (existing sessions keep their recorded provider): claude-sdk (Agent SDK driver — interactive permission cards, live mode/model switching) or claude (line protocol). Overrides the default_provider config key. |
--enable-eruda |
off | Enable the Eruda mobile-devtools quick action (loads from CDN) |
-v, --version |
— | Print version and exit |
Environment variables
| Variable | Purpose |
|---|---|
PAINAPPLE_CODE_HOME |
Override the data directory (default ~/.painapple-code). Set to /data in Docker. |
PAINAPPLE_CODE_CONFIG |
Override the config directory (default ~/.config/painapple-code) — where config.yaml and the auto-generated TLS cert/key live. |
BRIDGE_ALLOWED_ORIGINS |
Comma-separated CORS allow-list. Defaults to localhost on ports 8765/8800/8880. Set this when serving from a public hostname. |
Color presets
| Preset | Hex |
|---|---|
blue |
#58a6ff |
green |
#22c55e |
red |
#f87171 |
orange |
#fb923c |
purple |
#c084fc |
cyan |
#06b6d4 |
gray |
#9ca3af |
yellow |
#facc15 |
pink |
#f472b6 |
teal |
#14b8a6 |
indigo |
#818cf8 |
lime |
#a3e635 |
Any hex color also works: --accent '#e11d48'.
Highlighted features
Interactive permissions (Agent SDK provider)
By default the bridge drives Claude Code through the official Claude Agent SDK rather than a bare claude -p pipe (the raw stream-json is still teed verbatim, so sessions stay resumable in the plain CLI). This opens up the SDK's control plane, which unlocks a genuinely interactive approval flow.
- "Ask" mode — every tool call pauses on an inline approve / deny card. Cards render a human-readable preview of what's about to run (Write shows the file and content, Edit/MultiEdit an old→new diff, Bash the command) instead of a raw JSON blob, plus a live elapsed-wait timer and a deny box where your typed guidance is fed back to the model.
- "Always allow" shortcuts — cards surface the engine's own rule suggestions (allow this tool, trust this directory, or switch mode) so you can widen permissions in one tap.
- Live mode & model switching — changing the permission mode (YOLO, Accept Edits, Auto, Don't Ask, Plan, Ask) or the model now applies to the running process over the control plane, effective immediately even mid-turn — no more killing and restarting the session just to change levels. The Stop button interrupts gracefully and keeps the process warm, so the next message skips the
--resumecost. - Paused-turn UX — a background tab waiting on an approval gets an attention badge and a click-to-go toast, and the activity strip shows an amber "Waiting for permission…" lock with a frozen timer.
The classic line-protocol claude provider is still available (--default-provider claude) and keeps its static permission modes for line-only environments.
Shadow Git with auto-journal
After each turn the bridge runs a Haiku-summarization fork in the background and commits all file changes to a per-project shadow git repository (respecting .gitignore). The summary is parsed into structured fields (work done, decisions, learnings, problems solved, files changed, …) and stored in a local DuckDB so future sessions can query it.
The optional shadow-git-helper agent knows how to explore that history. After installing it (see Optional helpers) you can write prompts like "Consult with shadow-git-helper about X" and Claude spawns a sub-agent that pulls the historical context — keeping your main thread focused on the current task instead of bloating it with research.
This is not a backup. The shadow commit happens after the Haiku fork finishes, so a crash in between leaves a gap. Use it for "what did I change last time?" not "restore from disaster".
Comments stash
Click the bubble icon next to any paragraph/list item/heading and the message you type gets attached, with the selected quote, to your next prompt.
Per-turn summary bar
After each turn an inline bar shows context usage (filled %, this-turn gain, token count), token delta, files changed (with +/- diff stats and git status), tools used count, duration, cost, and which model ran. Click to expand the full breakdown. File pills are clickable on their own — tap for diff/preview, long-press for compare presets.
Embedded terminal
Real PTY via xterm.js, toggle with Ctrl+\. On mobile there's a keyboard extension bar with Ctrl/Alt/arrows above the iOS keyboard, long-press for shortcut popups, and a virtual d-pad joystick on touch-and-hold.
Markdown quick edit
Open any markdown file in the file preview, press e, and click any block — paragraph, heading, list item, blockquote — to edit the raw markdown in place. Enter saves to disk, Shift+Enter adds a newline, Esc cancels. Task-list checkboxes (- [ ] / - [x]) toggle through the same plumbing.
Prompt history + favorites
Heart any prompt to favorite it. The history explorer (Alt+P or Ctrl+R) searches every prompt across all sessions and projects with phrase, exclusion, date, and content filters. From results you can reuse, fork into a new session, or jump back to the original message.
Snippets and agents picker (#)
Type # in the chat input to insert a snippet or trigger an agent (built-in, global, or project-local). Sibling triggers on the same toolbar: / slash commands, @ files, $ skills.
Browser widget
A floating panel that renders local HTML files or loads external URLs through a same-origin proxy that strips frame-blocking headers and rewrites sub-resources. Pages run in a sandboxed iframe with no access to bridge cookies or auth. Per-session URL and back/forward history, like the terminal.
And more
Multi-session tabs (up to 5), git widget, cost analytics (Alt+4), file explorer (Alt+F), inline file/skill/agent suggester, JSON / CSV / Markdown rendered preview, multi-OAuth tokens configurable per session, paste-to-annotate image editor (draw on a pasted screenshot before upload), and live density modes (compact / normal / spacious) switchable from the status bar.
Optional helpers
A small shadow-git CLI, a shadow-query DuckDB-query wrapper, and the shadow-git-helper Claude agent template ship with the repo. The Docker image installs all three at build time, so containerised deploys get them for free. On a host install, run the script yourself:
src/painapple_code/tools/install-helpers.sh # install (skip if already present)
src/painapple_code/tools/install-helpers.sh --update # refresh from repo
src/painapple_code/tools/install-helpers.sh --uninstall # remove
src/painapple_code/tools/install-helpers.sh --dry-run # preview
This copies:
| Source | Target |
|---|---|
src/painapple_code/tools/shadow-git |
~/.local/bin/shadow-git |
src/painapple_code/tools/shadow-query |
~/.local/bin/shadow-query |
src/painapple_code/tools/agents/shadow-git-helper.md |
~/.claude/agents/shadow-git-helper.md |
No sudo, no $PATH edit. The agent invokes the CLI via the absolute path ~/.local/bin/shadow-git, so it works the same in Docker, VMs, Codespaces, and WSL.
Vendored frontend assets
CodeMirror 6 and xterm.js are served same-origin from static/vendor/, not loaded from a CDN. The bundles are checked in so a fresh clone or Docker build runs without any frontend build step.
To upgrade those libraries, bump versions in tools/package.json and run:
cd tools && npm install
npm install triggers a postinstall hook that regenerates static/vendor/{codemirror.js,xterm.js,...}. Commit the regenerated files alongside the lockfile change. See static/vendor/README.md.
Data storage
All bridge data lives under ~/.painapple-code/ (or $PAINAPPLE_CODE_HOME if set):
~/.painapple-code/
├── config.json # Global settings
├── shadow.duckdb # Turn metadata database
├── logs/ # Server logs (auto-rotating)
└── projects/{hash}/ # Per-project data
├── sessions/{id}/ # Session messages, metadata, uploads
└── shadow-git/ # Per-project shadow git repository
Auth config lives separately at ~/.config/painapple-code/config.yaml (mode 0600) so it survives wiping the data directory.
Known weaknesses
This is an MVP — there are tradeoffs.
- Windowing system — works, but doesn't support multiple instances of the same widget and could use a rethink.
- Code editor — currently a notepad with syntax highlighting. The plan is a review-driven workflow rather than a VSCode-grade editor; the markdown inline editor is the exception and works well for plan/doc tweaks.
- GUI for OS-level features (git widget, file explorer) — exists but the maintainer prefers the embedded terminal for
grep/sed/find/du, so these widgets have not been a priority.
License
AGPL 3.0 — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file painapple_code-1.0.0rc2.tar.gz.
File metadata
- Download URL: painapple_code-1.0.0rc2.tar.gz
- Upload date:
- Size: 9.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
840898a6b1e2dbcd13007469a2f38e6d7c476e0da7147ebf8238429bed489359
|
|
| MD5 |
2f6d945a7fa2aa0b9bac5638426075c7
|
|
| BLAKE2b-256 |
26ba445d33ccbaec43d35cfe755e4373fa7375fd7ef1d1a7f5031fb00a570a09
|
Provenance
The following attestation bundles were made for painapple_code-1.0.0rc2.tar.gz:
Publisher:
pypi-publish.yml on wrotek/painapple-code
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
painapple_code-1.0.0rc2.tar.gz -
Subject digest:
840898a6b1e2dbcd13007469a2f38e6d7c476e0da7147ebf8238429bed489359 - Sigstore transparency entry: 2130699194
- Sigstore integration time:
-
Permalink:
wrotek/painapple-code@f90e103aabc041fc78e0435d320ad22cc5e29a77 -
Branch / Tag:
refs/tags/v1.0.0-rc2 - Owner: https://github.com/wrotek
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@f90e103aabc041fc78e0435d320ad22cc5e29a77 -
Trigger Event:
push
-
Statement type:
File details
Details for the file painapple_code-1.0.0rc2-py3-none-any.whl.
File metadata
- Download URL: painapple_code-1.0.0rc2-py3-none-any.whl
- Upload date:
- Size: 2.1 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7a388c8a34636e5586c94e329edf6f947858810a8c8fc97c97b314b6d82bae0a
|
|
| MD5 |
763382b2be62ebbe7a222c2861a61b0f
|
|
| BLAKE2b-256 |
7394e412e133581dd9ddf96412278aa0c6e14d9cf73dcb4fb6aad10f5f54b9a0
|
Provenance
The following attestation bundles were made for painapple_code-1.0.0rc2-py3-none-any.whl:
Publisher:
pypi-publish.yml on wrotek/painapple-code
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
painapple_code-1.0.0rc2-py3-none-any.whl -
Subject digest:
7a388c8a34636e5586c94e329edf6f947858810a8c8fc97c97b314b6d82bae0a - Sigstore transparency entry: 2130699328
- Sigstore integration time:
-
Permalink:
wrotek/painapple-code@f90e103aabc041fc78e0435d320ad22cc5e29a77 -
Branch / Tag:
refs/tags/v1.0.0-rc2 - Owner: https://github.com/wrotek
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi-publish.yml@f90e103aabc041fc78e0435d320ad22cc5e29a77 -
Trigger Event:
push
-
Statement type: