The Epiq you love, with batteries included.
Oh-My-Epiq (OME) is a local-first, Git-backed issue board with four faces, a CLI (ome), a terminal UI, a browser board (ome gui) and an
agent MCP server (ome-mcp). It is a hard fork of
Epiq by @ljtn:
the same engine, the same event model, the same interfaces, with the operator
and agent tooling we wanted layered on top.
State is an immutable, append-only event log, versioned and synchronized over Git. No accounts, no SaaS, no database. Your board lives next to your code and travels with it.
Every upstream Epiq feature, plus the operator and agent tooling:
- Four surfaces, one state. CLI, TUI, browser board and MCP all read and write the same event log, so an agent's change shows up on your board.
- A migration from Epiq.
ome migraterenames the on-disk surface (~/.epiq-global→~/.ome-global,.epiq/→.ome/,__epiq_state__→__ome_state__) with a dry run, a backup and a printed rollback. Dry-run first; see Coming from Epiq?. - A clean agent MCP. One
ome_issue_updatereplaces seven narrow writers. Reads takeview:brief|fullwith brief by default. Every write takes a full id, a 7-charrefor a name. Adding a tag or an assignee a ticket already has is an idempotent no-op. Errors say what to pass. The everyday set registers by default and the administrative set opts in withOME_MCP_FULL=true. See MCP for agents. - A theme switcher. 23 bundled palettes ported from Oh-My-Pocket, opened
from the rail or phone tab bar. Picking one updates CSS variables on
:rootand re-reads every JavaScript-driven colour at render time, so the whole board repaints without a reload. A pre-paint guard stops any flash of the wrong look on first load. - The Oh-My-Pocket look. The browser board uses the same shell as Oh-My-Pocket: the Tokyo Night palette, square tiles, 2px window borders with Hyprland's gaps, no glow, a rail of icon tiles and bundled JetBrains Mono everywhere. The agent tooling and the board match, on the desktop and on the phone.
- The board on your phone. Under 760px wide the GUI turns into a phone app: one lane per screen, a ticket as a full-screen sheet with a lane picker, the event log as its own tab, and search through the command palette. Add it to the home screen and it runs standalone. See Remote and mobile.
- Remote access, signed in.
ome gui --access lan|tailscale|cloudflareor--allow-hostbehind a proxy such astailscale serve. Anything that is not this machine's own browser needs the owner token, and a phone signs in by scanning a single-use pairing code. - The MCP server is all an agent needs. It sends the board workflow
(take an identity, sync on demand, keep tickets current) to every client on
connect, so a project carries no agent files. A longer written skill is
offered at
skills/ome/SKILL.mdfor anyone who wants it. - Ticket notes. A section beside comments for gathered context: websearch results, background reading, anything appended without disturbing the description or the discussion. Notes sync like everything else.
- An Ideas board in every project.
initseeds one beside Default, and the board tabs above the lanes switch between boards. Ideas are tickets like any other. Triage one onto a working board when it has a plan. - A project picker. The header names the project behind the board, and opening it lists known projects plus an absolute-path field. Folders without OME state land on the init screen; a descriptor that never recorded its genesis offers a one-click repair.
- A collaboration board for agents. Notes, assistance requests and
handoffs with open, claimed and resolved, plus pins. Global lines sync to
every clone through the state branch; local lines stay on the machine that
wrote them. Behind the
OME_MCP_FULLgate, taught by the skill.
The engine is Epiq's; the operator and agent tooling on top is OME's. In one glance, what this fork adds:
- A migration, not a re-install.
ome migratecarries an existing Epiq board to OME's on-disk layout with a dry run, a backup and a printed rollback. - A pruned, forgiving agent MCP. One update tool, brief reads by default, ids or refs everywhere, idempotent no-ops, and a core toolset with the rest behind an opt-in flag. The board workflow ships to every connected client, so a project carries no agent files.
- Ideas, picker and collab. Every project starts with an Ideas board beside Default; the header switches projects without dropping to a terminal; agents coordinate on a collaboration board with global and local lines.
- The Oh-My-Pocket look, desktop and phone. The browser board uses the Oh-My-Pocket shell (Tokyo Night palette, square tiles, icon rail) and turns into a standalone phone app under 760px, with signed-in remote access over LAN, Tailscale or Cloudflare. Epiq's GUI has neither the shell nor the sign-in.
The fork ships the engine and the agent tooling first. These operator features are designed and tracked but not implemented yet:
- Global portfolio board. Every project's tickets in one view, across boards, with the active project highlighted.
- Lifecycle status. An explicit, ordered phase
(
idea · planned · in-flight · maintenance-staged · maintenance-done · support · parked) that drives a status view on the board. - Priority boards. Rank work P0–P3 without fighting the ticket schema. (Loose capture ships: every project starts with an Ideas board.)
- Instructions on a ticket. A structured "what to do" block, distinct from the description and the comment thread, so handoff to an agent is unambiguous.
- New-project creation from the WebUI. The picker opens existing
projects; creating one still needs the TUI for
:init. - Push notifications and offline use on the phone. Oh-My-Pocket's service worker is not ported yet; the phone app needs the server reachable.
The description in this README is the system of record for what ships; the roadmap above is the honest list of what does not.
- Workflow replay. Rewind the board and watch what happened while you were away, as if it were happening now.
- Code linked to tickets. Prefix a commit subject with a ticket's
refand the diff shows up on the ticket. Comment on selected lines; file a ticket straight from a selection. - Event-sourced, conflict-light state. Append-only per-user logs merged deterministically, so concurrent work converges.
- Repo-native, offline-friendly, fast, portable, command driven.
OME is not published to npm yet, and a direct
npm install --global git+https://github.com/T-Crypt/oh-my-epiq.git does not
work: the build runs on prepack, while a Git install runs prepare (which
only sets up husky), so the dist/ the package needs is never produced.
Install from a clone or a packed tarball instead.
From a clone. This builds dist/, then links the bins:
git clone https://github.com/T-Crypt/oh-my-epiq.git
cd oh-my-epiq
npm install
npm run build:npm
npm link # exposes `ome` and `ome-mcp`From a packed tarball. npm pack runs the build, then install it under a
pinned prefix:
npm pack # writes oh-my-epiq-<version>.tgz and runs the build
npm install -g --prefix ~/.local/share/ome-mcp-npm ./oh-my-epiq-*.tgz
# ~/.local/share/ome-mcp-npm/bin/{ome,ome-mcp}Verify:
ome --versionInside any Git repository:
omeFirst run opens the interactive setup wizard. That is it.
Launch the browser board with:
ome guiOn a machine with no display, or with --no-open, it serves without opening a
browser; open the URL it prints. Without a terminal attached it also stays shut,
so a script or an agent driving it never takes your desktop. OME_GUI_OPEN=1
opens one anyway (a keybind has no terminal). OME_GUI_NO_OPEN=1 never
opens one whatever else is set. The board serves on loopback by default
(port 3710). To open it from a phone or another machine, see
Remote and mobile.
Setup writes your user config to
~/.ome-global/config.json.Initialization creates:
- Project definition in
./.ome/project.json- Authoritative Git state at
~/.ome-global/worktrees/<id>(branch__ome_state__)- A
.gitignoreentry for local-only.ome/log/- A local debug log at
.ome/log/ome.log
Act as somebody other than the configured user (for an agent or a second operator):
ome --as claudeThe browser board works from a phone. It uses the same access model as Oh-My-Pocket. Pick how you reach the board, and sign each device in once.
| Access | Command | Who can reach it |
|---|---|---|
| This device only | ome gui |
Loopback. The default, as before |
| Behind a proxy | ome gui --allow-host https://host.example.ts.net:3710 |
Whoever the proxy lets in. Use this with tailscale serve |
| Local network | ome gui --access lan |
The machine's own LAN address, never every interface |
| Tailscale | ome gui --access tailscale |
The machine's tailnet address, plain http |
| Cloudflare Tunnel | ome gui --access cloudflare |
Anyone with the trycloudflare.com URL. Needs cloudflared on PATH. Public, so only the token stands behind it |
--allow-host takes a bare name or the URL a browser opens, and can repeat.
OME_GUI_ALLOW_HOSTS (comma-separated) does the same from a service unit, and
OME_GUI_PORT sets the port.
The recommended setup on a tailnet keeps the board on loopback and lets Tailscale terminate TLS:
OME_GUI_PORT=3710 ome gui --no-open --allow-host https://<machine>.<tailnet>.ts.net:3710
sudo tailscale serve --bg --https=3710 http://127.0.0.1:3710Signing in. The first time you reach the board from anywhere but its own machine, it shows a sign-in page. There are two ways in:
- Pair a phone. On a signed-in board, open Devices (the phone icon at the bottom of the rail, or Me on a phone) and tap Show a pairing code. Scan the QR code with the phone's camera. The code lasts 15 minutes and works once.
- Use the sign-in link. When the board answers to more than loopback,
ome guiprintshttps://…/login?token=…and its QR code at startup. That link is the owner token. Treat it as a password.
A signed-in browser keeps an HttpOnly cookie for a year. ome gui --rotate-token replaces the token and signs every device out. The token lives
in ~/.ome-global/gui-access.json (mode 600).
What is checked. A request is this machine's own browser only when it arrives on a loopback socket, names a loopback host, and carries no forwarding header. A proxy on the same machine still counts as remote. The server refuses a request naming a host it was not told about before any route, which stops DNS rebinding. Writes and the live socket must also come from a page this server served: the old loopback rule, or an allowed host whose origin matches the request. The server refuses a page on another port of the same machine even though it shares the cookie.
On the phone. Drag and drop does not work on a touch screen, so a ticket's
sheet has a lane picker. The command palette has a new Move ticket to a
lane command (move, as in the TUI), which works on the desktop too.
Everything else is the same board: the same tabs, comments, tags, assignees,
attachments, linked commits and history.
OME renames the on-disk surface, so point it at your existing boards once:
ome migrate # plan only, changes nothing
ome migrate --apply # back up, rename, move the state branch, verifyThe migration backs up ~/.epiq-global, creates __ome_state__ at the same
commit as __epiq_state__, rewrites .epiq/ → .ome/ and
~/.epiq-global/ → ~/.ome-global/, and prints a rollback. Environment
variables were renamed too (EPIQ_* → OME_*); the old names are still read
with a deprecation warning. A half-migrated board still reads: diff comments
accept legacy epiq- markers and commit refs accept both [epiq: and [ome:.
ome migrate is a one-way door. Always run the dry run first, and never run
--apply against a board you have not named.
OME ships an MCP server, ome-mcp, speaking stdio. Register it with Claude
Code:
claude mcp add --scope user ome -- /absolute/path/to/ome-mcpFor hand-edited clients:
{
"mcpServers": {
"ome": {
"command": "/absolute/path/to/ome-mcp"
}
}
}Tools are namespaced ome_* (ome_issue_create, ome_issue_update,
ome_board_list, …). Every write accepts a ticket by full id or by ref.
ome_sync is the only tool that touches the network; sync is on demand. Take
an identity (ome_actor_assume, provider/name) before your first write.
Core toolset by default. Reads, issue create/update/comment/note, sync,
and assume are always available. Set OME_MCP_FULL=true to enable the full
toolset including board/swimlane management, tag/contributor administration,
attachments, ome_project_init, and ome_skill_install.
ome_issue_list and ome_issue_get accept view:brief (default) for a
lean response or view:full for descriptions, comments, and notes.
The full toolset also carries the collaboration board: ome_collab_post
(note, request or handoff), ome_collab_list, ome_collab_claim,
ome_collab_resolve and ome_collab_pin. Global lines sync to every clone
through the state branch. Local lines stay on the machine that wrote them.
The server sends the board workflow as its MCP instructions, so nothing has to
be installed in a project. The longer written skill is optional: it lives at
skills/ome/SKILL.md, and ome_skill_install copies it into one clone at
.claude/skills/ome/SKILL.md and lists it in .git/info/exclude, so it is
never committed. See CONTRIBUTING.md.
OME uses Git in the background. No manual Git commands. ome writes to an
append-only event log on a dedicated state branch (__ome_state__) via a Git
worktree, isolated from your working tree. Running :sync (or ome sync)
merges and publishes. Project metadata lives in .ome/project.json.
Events use time-sortable ULIDs plus an edge reference to the last known event, so concurrent edits converge deterministically and merges stay trivial.
Oh-My-Epiq is a fork of Epiq by @ljtn. The engine, the event model and the original design are his. If this is useful, go star the upstream. See UPSTREAM.md for the full fork relationship.
MIT licensed. See LICENSE. The shell's look follows Pi Pocket by Tanner Middleton, through the Oh-My-Pocket fork. JetBrains Mono is bundled under the SIL Open Font License.
Never leave your editor.








