Skip to content

judell/bram

Repository files navigation

Bram

What is it?

A desktop app that helps you make best use of git and GitHub for AI-assisted software development.

Bram runs agents mindfully.

Who is it for?

Anyone who wants to use AI coding agents in a safe and accountable way.

Bram has opinions. It thinks versioning and collaboration are well-handled by git and GitHub, so it guides agents to make best use of them on your behalf, in conversation with you. And it thinks GitHub is great for accountability, so it also guides agents to join you in orderly and well-documented collaboration that leaves an auditable trail.

v28

Blog

How to make best use of git and GitHub for AI-assisted software development

Vibe coding as a team sport

How does it work?

Layout

  • Terminal — where you run an AI coding agent (e.g. claude or codex).

  • Agent pane — where Bram guides the agent through the workflow (Worklist, Issues, Sessions, Status, etc.).

  • Target app (optional, off by default) — a project iframe for previewing your app inside Bram. Most people run their own web app in their own server and view it in their own browser, so this pane is hidden unless you turn it on (Settings → UI → "Show target app"). It is handy for a very simple app, but it is not the common case.

When the optional target-app pane is enabled, Bram's file watcher reloads it automatically as project files change. That auto-reload is purely a convenience for the embedded pane — if you view your app in your own browser, your own dev server handles reloading.

Glossary

A small glossary of the terms used throughout this README, the conventions sidecar, and the UI. When the docs and the UI both use these terms consistently, agents echoing back to you stay consistent too.

Term What it refers to
main app the whole Bram desktop shell.
app toolbar the top button row in the main app (reload target app · devtools · agent tools · terminal · A− · A+ · voice).
terminal the xterm.js area where Claude / Codex runs.
target app the optional project iframe (off by default) for previewing your app inside Bram.
agent pane the area where Bram guides the agent through the workflow — the tabs UI (Worklist, Issues, Commits, Sessions, History, Context, Status, Settings).
agent nav the vertical menu on the left of the agent pane (Worklist, Issues, Commits, Sessions, History, Context, Status, Settings).
agent toolbar controls associated with the agent pane (per-view action bars: Refresh, Approve, etc.).

Workflow

Bram encourages an issue-first workflow as the foundation for a worklist item. It is optional, not required. If you want to use it, ask an agent to file an issue first, then use that issue as the basis for a proposed worklist item.

On the Issues tab, + New issue lets you ask Bram to file a GitHub issue. On the Workspace tab, + New item lets you ask Bram to propose a worklist item, with an optional picklist of open issues so a proposal can be anchored to an existing issue.

There are three phases for an item on the worklist: proposedappliedcommitted. The arrows between the phases are approval gates where you can dwell and iterate with your agent to: research and write code, create, refine, and close issues; organize commits as an orderly audit trail.

By default every change request flows through the worklist regardless of size. To opt out of the worklist for a specific request, include a phrase like "just do it", "no worklist for this", or "commit directly" in the same turn — see app/__shell/conventions.md for the full opt-out list and worked examples.

Agent conventions

Conventions are written in app/__shell/conventions.md. Claude is bound to that file directly through CLAUDE.md and the installed .claude hook/config path. Codex is bound through a repo-local AGENTS.md block installed by setup, with the shell startup seed as a backup for wrapped launches, then reinforced by the shared provider-neutral setup machinery plus local Codex config, memories, and rules.

Prerequisites

  1. A local git repo in which you develop your app

  2. git — usually preinstalled on macOS and Linux; install via your package manager if missing.

  3. GitHub CLI (gh) - Powers the Issues tab in the agent pane and the agent's issue create / close / comment operations. Install from https://cli.github.com/ and run gh auth login once. Without it, the Issues tab shows an empty state.

  4. XMLUI CLI - optional. If you are developing an XMLUI app, or if you are developing Bram itself (the agent pane UI is an embedded XMLUI app) you will want the XMLUI MCP server. Follow the steps here to get it.

  5. whisper-server — optional. Powers the 🎤 voice button in the parent-shell toolbar and the agent pane. Tested on macOS and Windows/WSL, see Voice input below for install and per-platform status.

Install

macOS / Linux

curl -fsSL https://github.com/judell/bram/releases/latest/download/install.sh | bash

The script detects your platform, verifies the archive's SHA256 against the published SHA256SUMS, extracts the binary, and copies it to /usr/local/bin (if writable) or ~/.local/bin. On macOS it also clears the com.apple.quarantine xattr. No sudo required.

Windows

powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://github.com/judell/bram/releases/latest/download/install.ps1 | iex"

Downloads bram-windows-amd64.zip, verifies its SHA256, extracts bram.exe to ~/bin, and adds ~/bin to your user PATH.

Smart App Control

On some Windows 11 setups, Smart App Control may block the unsigned binary — most users report no problem. If you do hit a block, you can disable SAC under Windows Security → App & browser control → Smart App Control settings. Before flipping the switch, read Microsoft's Smart App Control FAQ so you understand the consequences for your machine — the re-enable path has changed across Windows updates.

Agent tools

  • Worklist — the proposed → applied → committed approval surface that coordinates multi-step agent work. Each item is a small, independently approvable diff with a before → after summary. Select one item at a time (radio); three ghost actions act on it — Approve (TO APPLY → on-disk edits, transitions to TO COMMIT; TO COMMIT → git commit), Iterate (refine in place — agent revises the proposed text or edits the on-disk files per your feedback, item keeps its state), Drop (remove the item; for TO COMMIT, disk edits stay until you ask the agent to revert). Each row's + feedback link expands a per-item message-to-agent textarea that travels with whichever action you click. The agent never advances state unilaterally. Bram always writes local resources/worklist-history/ snapshots for auditability, while committing that directory is an opt-in repo policy.

    The Worklist view also carries the live agent conversation surface: pending permission menus, current-turn tool uses, the last agent response, pasted or dropped image previews, and the message box used to ask the agent for a new worklist item or follow-up.

  • Commits — HSplitter list of recent commits on the left, selected commit on the right. Full-history search via git log --grep across subject, body, and author; matched commits expand to clickable hit-row snippets, and the target app stacks snippetAroundLine previews for every hit. The target app header is an ExpandableItem revealing the full commit message body. Unpushed commits surface a "Push" button; it runs git push origin, and on a non-fast-forward rejection it fetches origin and rebases the branch on origin/<branch> before retrying (linear history, no merge commits).

  • Issues — HSplitter list of GitHub issues on the left (via gh issue list), selected issue on the right. Search runs gh issue list --search and tags hits per title/body line; clicking a hit filters the target app body to paragraphs containing the query. The expanded issue refetches every 30s so edits made via gh or github.com surface without collapse-and-reopen.

  • Sessions — HSplitter list of local claude/codex JSONL sessions on the left, selected session's turns on the right. Search runs server-side across user and assistant text; hits filter the target app to matching paragraphs. Each row has a ✕ delete (with confirm) and a ✎ rename: on Claude the rename appends a custom-title record to the session JSONL, on codex it appends a {id,thread_name,updated_at} entry to ~/.codex/session_index.jsonl. After the action, the row dims and the buttons disable until the next agent restart picks up the change. Codex's /resume creates a forked session with a new id, so the [current] marker won't follow a renamed codex session — the rename modal documents that caveat inline.

  • History — browse the resources/worklist-history/ snapshots Bram writes on every worklist change: the proposed → applied → committed audit trail, grouped by item phase, with summarized item prose so the reasoning behind past items survives after they're committed or dropped. Backed by /__worklist-history/list plus /__worklist-history/search for substring filtering.

  • Context — provider-aware HSplitter view of the active agent's durable local context sources. For Claude, that means CLAUDE.md, its @-imports, the per-project memory tree, hooks, and settings. For Codex, that means repo-local AGENTS.md when present plus Codex-side sources such as ~/.codex/config.toml, project-local .codex/ files, memories, and rules. Substring search shows grep-style hit snippets in the list and snippetAroundLine context on the right.

  • Status — a coordination-health view of the host↔agent plumbing: port-file consistency, loopback HTTP responsiveness, the inflight spinner sentinel, and the latest worklist authorization records. Use it to diagnose a stuck spinner or a refused loopback connection.

  • Settings — bidirectional view of .bram.json. Surfaces the agent-command (shell.agent), the worklist's batch-commit-actions toggle, and the show-target-app switch (off by default — reveals the optional target-app pane; otherwise the agent pane fills the right column). Edits persist to disk; hand-edits to .bram.json flow back through a settings-changed Tauri event without manual reload. Agent-command changes require a Bram restart to take effect (consumed at PTY spawn); the other settings apply live.

The four search-capable tabs (Issues, Commits, Sessions, History) all share a single <SearchBox> (250 ms debounce, ✕ to clear, inline spinner during fetch) and a single <SearchHitModal>. Clicking a hit opens the modal centered on the matched term within a 500-character window, with the match visibly highlighted. Diff renders across the Worklist TO COMMIT expander and the Commits per-file patch through a single <DiffView> that goes through a backend /__diff/annotate route for word-level intra-line emphasis.

The toolbar's (top-right of the agent pane's AppHeader) opens a target app info modal with the current URL, version, project-server config, and a README on GitHub link to this document.

Toolbar

  • ↻ reload xmlui app — force-reload the target app iframe (file watcher does this automatically, but useful after edits to the parent shell).
  • 🔍 browser devtools — open the WebView devtools for debugging the target app.
  • 🛠 agent tools — toggle the agent pane above.
  • ▢ terminal — toggle the terminal (hide it to give the web app full width). Window and splitter resizes preserve the terminal viewport instead of snapping scrollback to the top.
  • A− / A+ — decrease / increase the terminal font size (Cmd+− / Cmd+=).
  • 🎤 voice — toggle local-Whisper voice dictation into the terminal (Cmd+Shift+D). See Voice input.

Pinned across the top of the agent pane (stays reachable from any tab):

  • ⓘ info — show a target app info modal (URL, project-server status, "Open in browser").
  • A− / A+ — decrease / increase the target app / agent pane iframe font size.
  • 1 / 2 / 3 — send numeric keystrokes to the active agent's terminal session.
  • Yes / No — send "yes" or "no" as a complete user turn (handy for the agent's conversational prompts).
  • Esc — send Esc to interrupt the agent mid-response.
  • 🎤 voice — local-Whisper dictation; click to start, click again to send the transcript as a fresh user turn. Same engine as the shell toolbar's voice button.
  • 🔍 Inspector — open the XMLUI Inspector to reproduce a UI issue and export a trace JSON for analysis.

Provider-aware setup

Once you launch an agent through the wrapped terminal functions (claude or codex), the agent pane checks what that provider still needs for the current repo and prompts only when setup is missing.

Current behavior:

  • Claude in a fresh repo — prompt once. Setup installs the provider-neutral core plus the Claude-specific adapter.
  • Claude in a repo that is already set up — no prompt.
  • Codex in a fresh repo — prompt once. Setup installs the provider-neutral core, the codex hook adapter, and the codex developer_instructions, and it also refreshes the shared Claude-side artifacts that live in the repo.
  • Codex in a repo where setup has already run — no prompt. The repo and user-global Codex setup artifacts are already in place.

When the prompt runs, Bram installs two layers:

  • A provider-neutral core: Bram records the latest structured approved: / drop: payload in resources/.worklist-authorization.json and uses that local record when validating worklist removals. The desktop watcher can revert an invalid prune as a defense-in-depth fallback if a hook ever fails to fire.
  • A Claude adapter: .claude/hooks/worklist-guard.py, registered in .claude/settings.json to fire on Write|Edit. The hook denies edits to project files not covered by a proposed/applied worklist item (with explicit opt-out phrases in the last user message as the escape hatch), and validates worklist-prune authorization for changes to resources/worklist.json itself.
  • A codex adapter: ~/.bram/codex-worklist-guard.py, registered in ~/.codex/config.toml as a PreToolUse hook with matcher ^(apply_patch|Bash|Write|Edit|mcp__.*)$. Same coverage logic as the Claude hook, broadened to catch codex's apply_patch tool, mutation-shaped Bash commands, and MCP filesystem write/edit/create/move calls. Setup also writes developer_instructions into the codex config so the gate prose lands in the developer-role context part of every session, not just the user-role AGENTS.md. Existing ~/.xmlui-desktop/codex-worklist-guard.py installs remain accepted during migration; rerunning Setup rewrites the config to the Bram path.

In the Bram source repo, the Claude guard's source of truth is app/__shell/worklist-guard.py. The .claude/hooks/worklist-guard.py file is the installed runtime copy, refreshed from the source bundle by Setup and by src-tauri/build.rs during Cargo builds. Functional edits belong in app/__shell/worklist-guard.py; editing the installed copy directly creates setup drift and may be overwritten. The Codex guard uses the same source/installed split: app/shell/worklist-guard-codex.py is canonical, ~/.bram/codex-worklist-guard.py is installed.

PreToolUse hooks are the generic extension point — both Claude Code and codex expose them — so the two adapters share the same shape: each runs before the agent invokes a tool, receives a JSON payload describing the pending call on stdin, and can exit 0 to allow, return a deny decision to block (stderr/permissionDecisionReason goes back to the agent as a tool error), or fail to launch.

That means first-run setup is provider-aware in when it prompts but provider-symmetric in what it installs: launching either claude or codex and accepting the prompt sets up the shared core, the codex-side AGENTS.md guidance block, the codex developer_instructions, and the Claude and codex hook adapters.

How conventions.md governs both agents

app/__shell/conventions.md is the canonical project convention file. It governs Claude and Codex in different ways:

  • Claude: direct prompt binding plus enforcement. Setup copies that file to .claude/bram-conventions.md, adds an @-import block to CLAUDE.md, and installs the worklist-guard.py PreToolUse hook. A new Claude session therefore reads the conventions file directly and is also mechanically blocked from unsafe worklist edits. Existing projects with the legacy .claude/xmlui-desktop-conventions.md path are migrated to the new name on the next Setup run.
  • Codex: repo-local AGENTS.md plus native hook enforcement. Setup writes a marked Bram block into repo-root AGENTS.md, installs top-level developer_instructions in ~/.codex/config.toml, and registers the codex worklist guard as a native PreToolUse hook. Wrapped codex launches also receive the same concise worklist guidance as a startup seed. The app reinforces that with the shared local authorization record in resources/.worklist-authorization.json and the watcher-revert fallback as defense in depth.

So the practical rule is: both agents are governed by the same worklist conventions, with Claude reading the imported conventions file directly and Codex receiving the equivalent guidance through AGENTS, top-level developer_instructions, and its native hook adapter.

Claude and Codex also differ in how they call Bram's worklist lifecycle routes. Claude uses the loopback HTTP endpoints directly. Codex's sandbox refuses loopback connections (curl: (7) even when Bram is listening, #130), so it drives the identical lifecycle over a filesystem channel instead — writing resources/.worklist-intent.json and reading resources/.worklist-result.json, which the host dispatches through the same handlers as the HTTP routes.

The provider hooks validate direct edits to resources/worklist.json. Proposal authoring and iterate-time prose refinement are allowed there; mechanical prune / status-advance operations are expected to go through POST /__worklist/mutate instead. Both providers now reject direct worklist edits that remove items or change their status, which keeps the shared backend endpoint as the canonical state machine for advance / prune. The watcher-based fallback (compare old/new worklist snapshots, consult resources/.worklist-authorization.json, restore prior contents if the prune wasn't authorized) remains as defense-in-depth — it fires later than a native hook, but it covers the case where a hook fails to launch (e.g., Python missing) or where a future provider integration lacks a comparable extension point.

The hook is a Python script and needs Python 3 to run. On macOS and Linux it's invoked directly via its shebang (#!/usr/bin/env python3), so python3 must be on PATH — almost always the case. On Windows it's invoked via py -3 <path>; the py launcher ships with the python.org installer and resolves Python via the Windows registry, independent of PATH. If Python isn't installed at all, Claude Code shows "Failed with non-blocking status code" for every Write/Edit and the validator is silently inert — writes still proceed, but the worklist guard isn't actually checking them. Install Python 3 to enable enforcement.

Build

The frontend is static — no bundler, no package.json. The only build step is the Tauri/Rust build.

From src-tauri/:

  • Dev: cargo run (or cargo tauri dev with the Tauri CLI)
  • Release: cargo tauri build

Tauri docs: https://tauri.app/develop/, https://tauri.app/distribute/.

Calling Bram from project code

Because the target app is same-origin with the parent shell (tauri://localhost), project code can reach the Tauri command bridge directly through window.parent — no postMessage shim needed:

const { invoke } = window.parent.__TAURI__.core;
const url = await invoke("get_right_pane_url");

Use this when an XMLUI app embedded in the target app needs to read filesystem state, hit one of Bram's __-prefixed loopback endpoints, or invoke any of the Rust IPC commands. The helpers.js script loaded by the embedded XMLUI surfaces (toShell, toTurn, openExternal, logToHost) is built on top of this bridge — opt into the helpers for project XMLUI apps that need to talk back to the running agent.

Layout

  • Main.xmlui, components/, resources/, Globals.xs, config.json, index.html — the XMLUI app at the repo root.
  • app/ — parent shell (Tauri webview entry, terminal wiring, vendor scripts, and __shell/helpers.js that the target app includes).
  • src-tauri/ — Rust backend (PTY for the terminal, custom tauri:// URI scheme handler that proxies the target app iframe to the project's HTTP server, filesystem watcher, IPC handlers).
  • scripts/ — auxiliary scripts.

Voice input

Bram supports two ways to dictate instead of type:

  • 🎤 Whisper buttons (recommended). Local, low-latency dictation via whisper-server. Click the 🎤 button in the parent-shell toolbar (or the agent pane) to start recording, click again to send; the transcript arrives in the terminal as a voice: ... line so it's distinguishable from typed input. This is the better experience — lower latency, your choice of model, good transcription quality — but it needs local setup.
  • The agent's native /voice command. No local setup, but support varies by agent and platform. It's the zero-install fallback, and the working path where the Whisper button isn't proven yet.

Bram spawns the local whisper-server on the first record click and kills it on app exit — you don't manage the process; you just need the binary, ffmpeg, and a model file installed.

macOS

brew install whisper-cpp ffmpeg
mkdir -p ~/.local/share/whisper-models
curl -L -o ~/.local/share/whisper-models/ggml-small.en.bin \
  https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.en.bin

small.en is ~466 MB, English-only, real-time on Apple Silicon. Swap in a different model from the same Hugging Face repo for other size/accuracy/language tradeoffs. The bundled Info.plist declares NSMicrophoneUsageDescription, so first use triggers the standard macOS mic-permission prompt. The model path the app loads is ~/.local/share/whisper-models/ggml-small.en.bin.

Windows / WSL

On Windows, Bram launches whisper-server inside WSL via wsl.exe bash -lc and talks to it through http://127.0.0.1:18080 from the WebView. WSL2 forwards that loopback port to Windows automatically, so the request path is the same as on macOS once the server is running.

Prerequisite: WSL2 with Ubuntu. If you don't already have it, open PowerShell and run wsl --install, which installs WSL and the default Ubuntu distro. Restart when prompted, then complete Ubuntu's first-run setup (pick a Linux username and password). Microsoft's full install doc: https://learn.microsoft.com/en-us/windows/wsl/install.

One-time setup inside Ubuntu. Open Ubuntu (Start menu → Ubuntu, or run wsl from PowerShell), then paste this whole block. The cmake build takes a few minutes on modern hardware; the model download is ~466 MB.

sudo apt update
sudo apt install -y build-essential cmake ffmpeg git curl
git clone https://github.com/ggml-org/whisper.cpp.git
cd whisper.cpp
cmake -B build
cmake --build build -j --config Release
sudo cp build/bin/whisper-server /usr/local/bin/
mkdir -p ~/.local/share/whisper-models
curl -L -o ~/.local/share/whisper-models/ggml-small.en.bin \
  https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.en.bin

That's the whole install. Bram handles starting/stopping whisper-server on its own — first 🎤 click after launching Bram spawns it inside WSL (the button shows ⏳ for ~2-3 s while the model loads), and every click after that is fast until Bram exits.

Notes:

  • Mic permission. WebView2 inherits the standard Windows microphone prompt. On first 🎤 click Windows asks to allow mic access; click Yes.
  • Multiple WSL distros. If Ubuntu isn't your default distro, set BRAM_WSL_DISTRO=Ubuntu (or whatever distro name) in your Windows environment before launching Bram. Single-distro setups need no env var.
  • Already running? If you happen to have whisper-server listening on port 18080 (e.g., started manually in another terminal), Bram detects it via a preflight probe and uses it instead of spawning a new one — no conflict.
  • Sanity check after install. From inside Ubuntu, which whisper-server should print /usr/local/bin/whisper-server, and ls ~/.local/share/whisper-models/ should show ggml-small.en.bin. If both look right, you're done.

Linux

The same setup is expected to work on non-WSL Linux, using the host process path and port 18080.

Screen capture

The screenshot helper currently exists but is not surfaced in the default Codex-themed UI. When enabled, it grabs an interactive rect-select screenshot, writes the PNG to the OS app cache, and injects Read this screenshot: @<path> into the terminal as a fresh user turn so the agent picks it up via its Read tool. No install ceremony — it shells out to a system binary.

Currently macOS-only: the implementation invokes /usr/sbin/screencapture -i, which ships with macOS. On Linux and Windows it returns "screenshot capture is currently macOS-only"; if you want a port (e.g. via grim / slurp on Wayland or a PowerShell snippet on Windows), please open an issue.

Configuration

.bram.json at project root is the primary config file. Legacy .xmlui-desktop.json is still accepted as a compatibility alias from Bram's prior name.

Startup

You can specify how to launch the agent in the terminal.

{
  "shell": {
    "agent": "agent --continue"
  }
}

agent dispatches to claude or codex at shell startup. Set BRAM_STARTUP_AGENT=claude or BRAM_STARTUP_AGENT=codex to pin the default; if unset, the shell falls back to codex when available and otherwise claude.

Working with a real backend

Bram binds the target app HTTP server to 127.0.0.1:<random-port> (it uses port 0 and lets the OS pick). That's fine for projects that talk only to public APIs or static files. It breaks when your project needs a fixed origin — OAuth callbacks, CORS allowlists, hardcoded API base URLs.

Compatibility note. The target app is an iframe. Backends that send X-Frame-Options: DENY or Content-Security-Policy: frame-ancestors 'none' (common for security-sensitive admin UIs) cannot be loaded into the target app regardless of port. Workarounds: configure the backend's dev mode to relax those headers, or serve the UI files via a permissive dev server (e.g. npx http-server) while keeping the real backend running for API calls. Otherwise, open the project in a standalone browser.

Declare a project server

Add .bram.json at the project root:

{
  "server": {
    "command": "python3 -m http.server 8080",
    "cwd": "xmlui",
    "port": 8080,
    "path": "/"
  }
}
field meaning
command shell command to bring up the project's server. Run via sh -c (Unix) or cmd /C (Windows).
cwd working directory for the command, relative to the project root. Optional; defaults to the project root.
port TCP port the iframe should target. Bram probes this port at startup.
path URL path appended to http://localhost:<port> for the iframe. Optional; defaults to /.

At startup, Bram:

  • probes 127.0.0.1:<port>. If it's already listening, it logs a notice and reuses the running server (useful when you start the server manually for log visibility);
  • otherwise spawns command in cwd, with stdout/stderr forwarded to Bram's own stderr (prefixed [server]);
  • waits up to 5s for the port to come up, then points the target app iframe at http://localhost:<port><path>. The iframe retries once on load error to absorb the case where the server takes a moment to bind;
  • on app exit, kills the spawned child.

The agent pane continues to load from Bram's internal loopback server regardless of this setting.

The app-under-test does not need to be an XMLUI app — .bram.json is Bram's own config file, separate from XMLUI's config.json. Legacy .xmlui-desktop.json remains supported.

URL parameters

Use query strings to parameterize the frontend without rebuilding — e.g. ?city=santarosa to switch tenant. Pass them on the command line to your server's command or bake them into path (e.g. "path": "/?city=santarosa").

Working example

community-calendar uses .bram.json for GitHub-OAuth-via-Supabase development. See docs/app-architecture.md for the Supabase URL-Configuration setup that requires the fixed localhost:8080/** origin.

Fallback: the redirect pattern

If you can't add a config file (e.g. you're working in a repo you don't own), you can still target a fixed origin by adding a self-redirect at the top of the project's index.html:

<script>
  if (location.hostname === '127.0.0.1' && location.port !== '8080') {
    var devQuery = location.search || '?defaultParam=value';
    location.replace('http://localhost:8080' + location.pathname + devQuery + location.hash);
  }
</script>

Run your frontend on a known port in a separate terminal (python3 -m http.server 8080) and launch Bram from the project root. Its iframe loads the random-port URL once, your script bounces it to localhost:8080. .bram.json is the preferred mechanism — it auto-spawns the server, surfaces logs, and doesn't pollute the project's HTML.

Service workers don't register on macOS/Linux

The target app iframe loads at tauri://localhost, and the WebKit engines on macOS (WKWebView) and Linux (WebKitGTK) don't treat custom-scheme origins as secure contexts. Service-worker registration silently fails there, so project features that depend on a service worker — Mock Service Worker (MSW), XMLUI's in-page apiInterceptor, custom offline caches — won't activate inside Bram on those platforms. Windows uses WebView2 (Chromium) with the http://tauri.localhost form, which is a secure context, so service workers register normally there.

Apps that hit a real HTTP backend are unaffected; the constraint only applies to in-page request interception. If you're developing against MSW or apiInterceptor, run your project in a regular browser tab at localhost:8080 while keeping Bram pointed at the same server for the agent loop.

Auth callbacks won't reach the target app

The target app webview has its own browser storage, isolated from your system browser's storage at the same origin. That breaks any auth flow that hands off to the system browser and expects a session to come back into the webview:

  • Magic links in email. Clicking the link opens your default browser, completes auth there, and stores the session in the browser's localStorage. The target app never sees it.
  • OAuth provider redirects that leave the webview have the same shape — the callback session lands in the wrong storage.

Even when the redirect script above lines the target app up on localhost:8080, that origin's storage in the Tauri webview is a different store from localhost:8080 storage in Safari or Chrome.

Workaround for email auth: send a one-time code, not a link. If your backend supports OTP codes (Supabase, Auth0, Clerk, Cognito all do), have the user paste the code from the email into a field in your dialog. No callback URL, no cross-context handoff. Works identically in the browser and inside Bram.

For Supabase specifically:

  1. Add {{ .Token }} to the Magic Link email template (Supabase dashboard → Authentication → Email Templates) so the email includes the 6/8-digit code. Docs: https://supabase.com/docs/guides/auth/auth-email-templates
  2. After signInWithOtp, render a code-input field and call verifyOtp({ email, token, type: 'email' }). Docs: https://supabase.com/docs/guides/auth/auth-email-passwordless
  3. The existing onAuthStateChange handler fires on verifyOtp success — no other plumbing needed.

community-calendar implements this in xmlui/components/SignInDialog.xmlui and xmlui/shell.js (window.signInWithEmail + window.verifyEmailOtp).

DevTools

Tauri uses the platform's native webview, so the DevTools you get inside the target app depend on the OS:

Platform Webview DevTools
macOS WKWebView Safari Web Inspector
Linux WebKitGTK Safari Web Inspector
Windows WebView2 (Chromium) Chromium DevTools

To open them, right-click inside the target app → Inspect Element in dev/debug builds (cargo run or cargo tauri dev). Release builds disable DevTools by default. The execution context belongs to the target app document specifically. The shell window and the right pane both load at tauri://localhost (the parent shell directly, the target app via the scheme handler that proxies project content under /__project/*), so they share an origin and therefore a localStorage / IndexedDB partition — a console session in either reaches the same storage. A regular browser tab pointed at the project's own localhost:8080 server, by contrast, is a different origin with its own independent storage.

WebKit quirks worth knowing

The macOS/Linux Web Inspector behaves differently from Chromium's DevTools in a few ways that bite when you're testing auth flows:

  • const/let redeclaration throws. Pasting const sb = … a second time in the same console session yields "Unexpected identifier 'sb'. Expected ';' after variable declaration." Chromium silently redeclares; WebKit doesn't. Wrap repeated snippets in an async IIFE ((async () => { … })();) so the bindings are scoped to each call.
  • Frame/context switcher is sparser. The dropdown that picks the execution context (top-level vs iframes) often won't expose every frame the page contains. Right-clicking inside the frame you actually want and choosing Inspect Element is more reliable than picking it from the dropdown.
  • Service-worker and storage panels are less complete than Chromium's. If you need to inspect IndexedDB or service-worker scope details, run the same project in a regular Chrome/Edge tab pointed at localhost:8080.

If you'd rather use Chromium DevTools on macOS/Linux, you can run your project in a regular browser tab pointed at its localhost:8080 origin — but remember that the tab's localStorage is a separate store from the target app's (the target app is at tauri://localhost, a different origin), so a session created there won't carry into Bram.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors