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 and is very YOLO-oriented. Because the bridge runs Claude in headless mode (-p), any approval prompt (like write or bash run) is auto denied. For that reason, best experience you have using 'Auto' or 'Bypass Permissions' modes.

Permission modes available (configurable per session in the settings popup):

  • Plan — read-only
  • Don't Ask (default) — 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

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 (see below).

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 git+https://github.com/wrotek/painapple-code
painapple-code docker           # first run opens the setup wizard
painapple-code docker pull      # fetch wrotek/painapple-code:latest (pull v0.3.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 git+https://github.com/wrotek/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 at ~/.config/painapple-code/config.yaml (mode 0600), then logs a bootstrap URL with the token embedded.

# Reveal the password
awk '/^password:/ {print $2}' ~/.config/painapple-code/config.yaml

# Rotate (delete → restart, new password generated)
rm ~/.config/painapple-code/config.yaml
pkill -f 'port 8765'

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; 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
--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

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.

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.0rc1.tar.gz (2.2 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.0rc1-py3-none-any.whl (1.9 MB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: painapple_code-1.0.0rc1.tar.gz
  • Upload date:
  • Size: 2.2 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.0rc1.tar.gz
Algorithm Hash digest
SHA256 877977bb0a64d6cdd22a6ad51e064fddc2832f3e6a1185aa1353114e2f7542ce
MD5 a5c7e676b6da2b1ff68adfeb37589ba4
BLAKE2b-256 fed0485f1e8943ffb7db8ca9016bcdd46ed2052575f4f681cffea19514f5eb36

See more details on using hashes here.

Provenance

The following attestation bundles were made for painapple_code-1.0.0rc1.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.0rc1-py3-none-any.whl.

File metadata

File hashes

Hashes for painapple_code-1.0.0rc1-py3-none-any.whl
Algorithm Hash digest
SHA256 27297b04480d53715f35da72ae71e2fe178bccb6a08b4de8159b05a0af44eb75
MD5 92405c9c261c1046bd30b90aa472a266
BLAKE2b-256 baec310c556388f8092c5b648844361655609b01c5366f858c4b0f01f1dec775

See more details on using hashes here.

Provenance

The following attestation bundles were made for painapple_code-1.0.0rc1-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