Skip to main content

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 cp or mv.
  • Auto — Claude's AI classifier gates each tool call
  • YOLObypassPermissions, 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 (see --tls): browsers show a one-time cert warning, and the native app accepts it without verification. Either way TLS here protects against passive snooping only, not an active MITM — and 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 a Set-Cookie)
  • Header Authorization: Bearer <password> (for curl / 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; clients accept it unverified (wire encryption only)
--tls-cert auto-generated TLS cert path (default <config-dir>/cert.pem)
--tls-key auto-generated TLS key path (default <config-dir>/key.pem)
--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 --resume cost.
  • 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.

Journal widget showing per-turn Haiku summaries, files changed and cost, grouped by session

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.

Selecting a paragraph, adding a note, and the stash attaching itself to the 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.

Collapsed per-turn summary bar with file pills, tool counts, cost and duration

Expanded summary bar showing the context-window breakdown and per-file session changes

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.

Embedded terminal running ls, git status and the project test suite

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.

Editing a list item in place in the rendered preview, then toggling a task checkbox

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.

Prompt history explorer with search filters, a favorited prompt, and reuse actions

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.

The # picker popup listing agents and user snippets above the chat input

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.

Browser widget rendering a local HTML test report in file mode

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.

File explorer and git widget open over a session, showing the working tree and uncommitted changes

Cost analytics widget with per-model and per-thread cost breakdown

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.

  1. Windowing system — works, but doesn't support multiple instances of the same widget and could use a rethink.
  2. 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.
  3. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

painapple_code-1.0.0rc3.tar.gz (9.7 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

painapple_code-1.0.0rc3-py3-none-any.whl (2.1 MB view details)

Uploaded Python 3

File details

Details for the file painapple_code-1.0.0rc3.tar.gz.

File metadata

  • Download URL: painapple_code-1.0.0rc3.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

Hashes for painapple_code-1.0.0rc3.tar.gz
Algorithm Hash digest
SHA256 45a2abe5618acc665cadd99403a212b295aac1b5d811954057876ff3702dfb0c
MD5 2695c1c45411a151db4c17bcf16c65e2
BLAKE2b-256 1017a50966aad704b4cd38022ee1346a51c01375645fc175ec26de65e2311ad5

See more details on using hashes here.

Provenance

The following attestation bundles were made for painapple_code-1.0.0rc3.tar.gz:

Publisher: pypi-publish.yml on wrotek/painapple-code

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file painapple_code-1.0.0rc3-py3-none-any.whl.

File metadata

File hashes

Hashes for painapple_code-1.0.0rc3-py3-none-any.whl
Algorithm Hash digest
SHA256 be7d0295c25095ec5d7eb2163fb57d2c3a8cddce1c68099bd96b57a517625d8c
MD5 a5e66a7a66983054132f7790cd7b4266
BLAKE2b-256 542b372ff861cab6feb091db923f7b9a8b6fc4faafd0dd4c60cb14c76ec99d71

See more details on using hashes here.

Provenance

The following attestation bundles were made for painapple_code-1.0.0rc3-py3-none-any.whl:

Publisher: pypi-publish.yml on wrotek/painapple-code

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page