Follow the thread.
English · 한국어
A local SQLite file of your Jira — so "which epic is stuck?" is one query, not an unaskable one.
gadak mirrors Jira and Confluence into one local SQLite file — issues, comments, history, wiki pages — indexed together and searchable locally. This window is where that work lives on your machine: triage it in the desktop app or a browser tab, or let a coding agent ask in plain SQL and point the same window at the answer. One binary, one app, no gadak account.
The mirror is a cache you can throw away. On a connected workspace, if this project stops tomorrow, you delete a directory and have lost nothing: Jira stays the source of truth.
▶ Open the live demo
— 534 issues, in your browser, right now.
Changelog
— what shipped.
A connected site needs one API token — it covers Jira and Confluence on the same site. A standalone workspace needs no Atlassian account at all.
You pick what it mirrors. The wiki is off until you ask for it, and when
you do, you name the spaces — gadak init --spaces ENG,PROD, or Settings →
Sources. Jira narrows the same way with --projects. Nothing pulls a whole
site down because you installed it.
macOS app, CLI included:
brew install --cask midagedev/tap/gadakCLI only — same UI in a browser tab via gadak serve:
brew install midagedev/tap/gadak-cliWindows: from the latest release,
download gadak_<version>_windows_amd64.zip (or windows_arm64), unzip, put
gadak.exe on PATH. The desktop zip (Gadak-<version>-windows-x64.zip) is
unsigned — if SmartScreen or Smart App Control blocks it, that is a missing
signature, not a virus finding; use the CLI zip. Do not turn Smart App Control
off.
Connect to Jira:
gadak init && gadak sync && gadak serveOr start with no tracker:
gadak init --standalone
gadak create "the thing I just noticed"
gadak servegadak serve prints the address — open http://gadak.localhost:7777 and you
should see your issues. Linux tarball, pairing, the signed macOS dmg:
Install.
gadak sql "select epic_key, count(*) from issues_full where resolved_at is null
and epic_key <> '' group by epic_key order by 2 desc"That last query is the point: JQL has no GROUP BY. "Which epic is actually
stuck?" is not a hard question — it is an unaskable one, until the data is a
file. docs/RECIPES.md has the rest.
Run that query now, nothing installed: Datasette Lite loads the demo snapshot in this tab and the SQL runs client-side.
Measured 2026-08-26 against a live Cloud site (3,296 issues; medians, CLI startup included — method, the re-measurement history, and the losing rows):
| Question | REST API | gadak |
|
|---|---|---|---|
| Simple filter, 100 issues | 583 ms | 19 ms | 31× |
| One issue with its full history | 710 ms | 28 ms | 25× |
| Free-text search | 543 ms | 41 ms | 13× |
Open issues per epic (GROUP BY) |
4,761 ms — 8 API pages, aggregated client-side | 22 ms — one query | 214× |
| A count over the change history | not expressible — ≈ 28 min of crawling | 14 ms | — |
The last two rows are the point: past a page size, JQL answers stop being slow
and start being unaskable. The API can hand you rows but not the aggregate, so
every GROUP BY becomes a paging loop in your code.
The corpus is not the one the previous table was measured on — the project was re-scoped between runs (7,166 → 3,296 issues), so the two runs are comparable each against itself, not row by row.
And the other side: the first full sync measured 26.4 s for 534 issues and 7.2 min for 2,865 (method and the losing rows), every watch tick costs ~4.7 s on a quiet site even when nothing changed, and the mirror trails Jira by one sync interval.
▶ 90-second tour of the paper list (GIF, 7 MB)
The window, in 90 seconds. Generated from e2e/demo/web-demo.spec.ts against the demo snapshot.
Status: 0.18, still 0.x. Sync, read API, write-through, desktop, web, CLI, and MCP are verified against a live site.
CHANGELOG.md.
Jira search is a network round trip, and the wiki is a second search. An
agent asked "what did we already fix, and what did we decide?" pages two
REST APIs. Same cause: the data is not a file.
docs/CONCEPT.md · docs/PAIN_POINTS.md.
⌘K is the one index — titles, bodies, comments, issues and pages. The chips on the list do not apply. That is why a comment-only word still finds the row.
Generated from e2e/demo/search-demo.spec.ts against the demo snapshot.
| For | Looks like | |
|---|---|---|
| App + Web UI | all-day triage | desktop app (no port) or gadak serve. j/k walk, x selects, s/a/l/c change status, assignee, labels, or comment from the list. |
| CLI + SQL | agents, scripts | gadak issue, gadak search (FTS, --jql, a Jira URL, --explain), gadak sql, plus the file |
Writes go through the origin, then the mirror refreshes. App and web: comment,
transition, assign, labels, priority, title. CLI: create (single or
--batch), attach, edit, comment, transition (--resolution),
assign, link, dev link / dev scan, fields --apply,
issue --editmeta, project create, and page create / page edit /
page comment for the wiki (pages, titles, bodies and comments all through
the origin). Hierarchy, item_refs, attachments: docs/CONCEPT.md.
The window keeps one paper metaphor across four palettes — light, a
neutral-cool dark, blue-black ink, and warm ember. The theme follows
the system unless you pick one, and it belongs to the workspace, not the
browser: gadak config set appearance.theme ink.
Colors are config: ui.tokens / ui.dataColors flow from the CLI into an open tab with no reload, and the keys a palette owns refuse an override instead of silently breaking the paper. Generated from e2e/demo/tokens-demo.spec.ts.
And two surfaces is not a closed list. Reading the mirror is one binary
call (gadak search --json, ~20 ms), and opening anything in the app is
one URL (gadak://view?issue=… — the scheme), so
whatever can do those two things becomes a surface. A launcher, say:
Each keystroke is one gadak search --json; Enter is the deep link. A saved view travels the same way — gadak views open prints its link.
That launcher exists: a Raycast extension that searches issues and wiki documents as you type, submitted to the Raycast Store. Until the review lands, one command installs it from the binary you already have (embedded, no checkout):
gadak raycast installThe macOS app has the same install as a button — Settings → Integrations
lists Raycast, the agent skill and MCP, shows what is already installed, and
runs the exact command it prints. Building on the extension itself:
contrib/raycast/. And with no extension at all, a
Raycast Quicklink pointed at gadak://view?issue={argument} covers the
open-by-key half.
Connected talks to Atlassian Cloud. Standalone (from 0.16) is a workspace
with no Atlassian account — a minimal Jira origin that travels with the
app. The mirror is a cache either way; every write goes through the origin.
On standalone the durable file is the origin's persist file — issuetap.db
in the workspace's origin folder (SQLite, WAL). Copy it while gadak is not
running (include the -wal/-shm sidecars), or
sqlite3 origin/issuetap.db ".backup dest.db".
Reads, writes, hierarchy, wiki, attachments and history work on both. Boards, sprints as a UI, Jira dashboards and Jira's notification inbox do not — those stay in Jira.
▶ The full matrix, with the footnote for every ✅
| Connected (Atlassian Cloud) | Standalone (from 0.16) | |
|---|---|---|
| Issue read and search (FTS, JQL, SQL) | ✅¹ | ✅¹ |
| Create, comment, transition, assignee, labels, priority | ✅ | ✅ |
| Due date, description, custom-field edits (from 0.16) | ✅² | ✅² |
| Hierarchy | ✅³ | ✅³ |
| Wiki documents | ✅⁴ | ✅⁵ |
| Attachments | ✅ | ✅ |
| History / time in status | ✅⁶ | ✅⁶ |
| Agent surfaces (skill, MCP, SQL) | ✅ | ✅ |
| Boards and sprints | —⁸ | —⁸ |
| Dashboards | — | — |
| Jira notifications | —⁷ | —⁷ |
- SQL and FTS are local.
--jql/ a Jira URL maps a documented subset onto the in-memory filter; clauses gadak cannot express are listed, never dropped. Sprint-by-name,WAS, cross-fieldOR, and custom fields are among the refusals; numericsprint =/sprint inandsprint in openSprints()are in the subset (decision 0007). - Dedicated endpoints for due date and description. Custom fields: kinds
text,number,date,option,user,multi_option/version_array, gated by the issue's editmeta and the configured field allowlist. Cascading selects and textarea custom fields have no editor. - Epic grouping (
epic_key, nearest hierarchy-level-1 ancestor) is first-class. Setting a parent is CLIcreate --parent/edit --parentonly — there is no RESTPUT {key}/parent. Sub-task create-meta flags are not surfaced, so create cannot tell that a type requires a parent. - Confluence Cloud is mirrored; page create, edit (title/body) and page comments write through it —
gadak page create|edit|comment,POST pages/,PUT pages/{id}/edit,POST pages/{id}/comment. - Pages sync from the in-process origin.
gadak page create|edit|commentand the REST verbs work here too; the UI has a page comment composer but no page editor yet. - Changelog is mirrored. Time in status is computed from
status_changed_at, not stored as a column. - Jira's notification inbox, rules, and email are not mirrored. gadak has its own watch-feed OS alerts on macOS and Linux.
- No board UI and no sprint column on the list. Sprint fields (
sprint_id/sprint_name/sprint_state) are in the mirror; SQL and JQL (sprint =/sprint in openSprints()) can query them. Theversionscatalog andfix_version_idsjoin the same way.
Linear. A Linear workspace mirrors and writes through the same
verbs: add a "linear" block (apiKey, optional teamIds) to the
workspace's config.json and run gadak sync --source linear. Writes
route by the mirror's source for the key — comment, transition (the
team's workflow states, id-keyed), summary/priority/due-date edits,
assign/unassign, and file attachments all pass through Linear's API and
refresh the mirror row. Not yet: label edits, clearing a due date, and
state history (status_changed_at stays NULL) each refuse honestly
rather than half-applying; inline comment media attaches the file and
drops only the body embed. Field mapping:
internal/linear/MAPPING.md.
Deliberate gaps on the write side, stated here so nobody finds them in production:
edit -mrefuses to destroy a formatted description.-mwrites plain text; when the description currently on the origin carries formatting a plain replace would drop — tables, headings, lists, links, mentions — the edit stops and names what it found.gadak edit KEY -m … --force-plainreplaces it anyway. (page edit -mhas the same guard behind--force.)- No sprint verbs. Sprint fields are mirrored (
sprint_id,sprint_name,sprint_state; SQL and JQL query them), but moving an issue between sprints or editing a sprint happens in Jira. - No worklog verbs. Log work through the pass-through instead:
gadak api POST /rest/api/3/issue/KEY/worklog --data @wl.json --write.
This is half the reason gadak exists. Reference: docs/MIRROR.md.
One paste per host: docs/AGENT_SETUP.md.
gadak skill installSchema and query patterns, no extra process. For hosts without a shell (Claude Desktop):
gadak mcp install claudePins this binary and workspace into the registration.
gadak init and gadak install-cli install that skill automatically when
~/.claude already exists. A file gadak did not write is left in place.
Twenty-six seconds, one serve, one terminal session (play the film ▶). A ticket is handed to an agent and the pane is closed — the work keeps running with nobody watching. A phone closes another issue from the same board. The desk comes back to the scrollback it left and a count that already moved, including what the phone did. Shot by two cameras at the same moment, not cut together from two takes: e2e/demo/record-hero.sh.
What the skill buys: a live Claude Code session drives the same workspace you are looking at — colors and a chart dashboard land in the open tab, no reload. Recorded from tools/tapes/claude-drive.tape.
Both installs (and the Raycast one) are also buttons in the macOS app, with install state shown honestly: Settings → Integrations.
Two takes from the same rig, each following one job to the end. Nothing in either is scripted but the opening sentence — the commands, the HTML and the recovery are the model's:
Recorded from claude-dashboards.tape and claude-tokens.tape against the demo snapshot. Full-resolution MP4s: dashboards · tokens.
Setup is not a screen an agent has to click, either. Every field the settings dialog edits is a CLI verb over the same validation:
gadak config listEvery editable path and its value.
gadak config set appearance.theme inkPer workspace, applied live.
SQL answers; the window presents. Filter on status_category /
priority_rank (1 = most urgent, 0 = unset), never on a display name —
Jira translates those per account, so priority = High is silently zero
rows on a Korean-language site. --jql is Jira's own language and stores
the literal, so prefer gadak sql when the filter is a rank or category:
gadak sql --no-header "select key from issues_full where status_category = 'inprogress'
order by status_changed_at asc limit 5" | gadak views open --keys -If you already have JQL, the clauses land as chips. This one keys on a project key and emptiness, not a localized name:
gadak views open --jql 'project = NMA AND resolution is EMPTY'
gadak views open writes a one-shot hash; the running app or serve tab applies it. The recording adds a priority clause — in --jql a priority or status name is matched as the literal string your Jira stores, which is localized, so the example above leaves it out. Generated from e2e/demo/agent-demo.spec.ts.
When the answer is a wall rather than a list, author a dashboard — one HTML document plus registered datasources, rendered sandboxed in the web tab: docs/DASHBOARDS.md.
gadak dashboards save registers the document and its datasources; the host runs the queries and pushes rows in, and a re-save swaps an open frame in about a second. Charts come from a locally served uPlot — no CDN, no CSP widening. Generated from e2e/demo/dashboards-demo.spec.ts.
For hosts without a shell (Claude Desktop), the same mirror is an MCP server. Ask the thing Jira cannot answer at all, because the wiki is a second search: "what do we know about X?" One index holds both, so the answer can put a ticket and the design doc that drove it in the same sentence.
Five tools; no writes to the mirror or to Jira. A host with a shell can use gadak sql instead. Setup: docs/MCP.md.
gadak views open is the "open in gadak" verb; gadak open KEY leaves for
Jira. The list box takes the same JQL paste as gadak search --jql; clauses
gadak cannot express are listed, never dropped. What JQL still cannot ask
stays in gadak sql and docs/RECIPES.md. gadak sql
opens the file mode=ro; MCP's gadak_query rejects anything that is not a
SELECT. gadak api is the pass-through for endpoints
the mirror does not model — read-only unless --write, never on MCP.
An agent that reads your mirror sends what it reads to whatever model it
talks to. gadak itself sends nothing (SECURITY.md). Scope
the mirror to what the agent should see. When gadak does touch the network
— sync, writes, the pairing model that lets a tailnet or a whole team share
one workspace — docs/NETWORK.md walks every connection
and its off switch.
The two brew lines are at the top of this page. Atlassian Cloud, or (from 0.16) a standalone workspace with no Atlassian account — a connected site needs one API token, which covers Jira and Confluence on the same site.
Windows: take gadak_<version>_windows_amd64.zip (or arm64) from the
latest release, unzip,
put gadak.exe on PATH. The desktop zip is unsigned — a SmartScreen block
is a missing signature, not a virus finding
(why, and how to check the sha256). Do not turn
Smart App Control off.
▶ The dmg, the Linux tarball, and pairing a second machine
macOS dmg: Gadak-<version>-arm64.dmg from the
latest release, drag to
Applications. Signed and notarized. First launch walks through site, email,
token and projects. The CLI ships inside the bundle, and macOS does not put an
app on your PATH:
/Applications/Gadak.app/Contents/Resources/bin/gadak install-cliPair another machine. Home gadak serve is the origin. On the home
machine, mint an offer (stdout is one offer line):
gadak pairing mint --label laptopOn the remote, paste the offer:
gadak --workspace laptop init --pairing-code-stdinConfirm with gadak --workspace laptop status (paired with "laptop").
gadak pairing list is the token table on home and one status line on the
remote. gadak pairing revoke laptop is home only. _home is this machine's
routing token, not a device (revoke refuses it; mint --label _home
rotates). --profile is an alias of --workspace. The gate is in
SECURITY.md.
Linux without Homebrew: take gadak_<version>_linux_amd64.tar.gz (or
linux_arm64) and checksums.txt from the
latest release. One
archive is the whole install — the web UI is inside the binary:
sha256sum --ignore-missing -c checksums.txt
tar -xzf gadak_<version>_linux_amd64.tar.gzOptional, to survive reboot (systemd --user): gadak install-service. Arch
Linux: a checked PKGBUILD in contrib/aur/gadak-bin
— makepkg -si there; not in the AUR yet. A Scoop manifest lives in
contrib/scoop, bucket unpublished.
Install script, source build, Docker, wiki mirroring, workspaces, upgrades:
docs/INSTALL.md.
Making it yours. Two axes, no forking: docs/EXTENDING.md.
Config: docs/CONFIGURATION.md. Enrichments:
docs/PLUGINS.md.
How it works. One binary, one SQLite file; incremental sync plus a
reconcile pass. docs/ARCHITECTURE.md. Why not an
extension or Forge app: docs/decisions/0003-local-process.md.
Good fit / bad fit. Daily search latency, an agent over tracker and wiki,
offline reads — yes. Boards, admin, a page editor in the UI, or a minute of
staleness — stay in Jira. CLI and REST already write wiki pages.
docs/CONCEPT.md.
How it compares. jira-cli talks to the live API per command. Linear is a
different tracker, and also a gadak source (see above). Rovo MCP searches both sources too, but it is hosted: no
aggregate, no offline, and every call spends tokens.
docs/FAQ.md.
More sources later. Confluence proved the spine is neutral. Next source,
ranked by demand: docs/project/ROADMAP.md.
CHANGELOG.md— what shippeddocs/INSTALL.md·docs/DESKTOP.md— install, first run, the desktop appdocs/MIRROR.md·docs/MCP.md·docs/AGENT_SETUP.md— SQL, CLI, REST, MCP, one paste per hostdocs/RECIPES.md— questions JQL cannot ask, as SQLSECURITY.md·docs/FAQ.md·MAINTENANCE.md— threat model, site load, who maintains thisdocs/EXTENDING.md·docs/PLUGINS.md— fitting gadak to your teamdocs/project/STATE_OF_PLAY.md·docs/CONCEPT.md·docs/PAIN_POINTS.mddocs/ARCHITECTURE.md·docs/project/UX_PRINCIPLES.mddocs/README.md— the rest of the docs
One person, currently. Weigh that — and the other side: the mirror is a
disposable cache of your own Jira, the 0.x contract is the three promises
in data-model.md (issues_full and the
RECIPES queries, gadak sql stdout, and gadak views open --keys -), the
license is Apache-2.0, and the file is plain SQLite. Hard questions:
docs/FAQ.md. What you do not have to take on trust, each with
the command that checks it: PROMISES.md.
CONTRIBUTING.md — and
docs/project/GOOD_FIRST_ISSUES.md to start. Bug reports
need your Jira deployment type (Cloud), the gadak commit, and the command you
ran. Never paste real issue data, tokens, or site URLs into a public issue.
Commit GDK-nnn keys resolve on the public backlog.
To file something, open a GitHub issue
— the maintainer mirrors it into the backlog.
Using gadak with an agent and hitting friction? Open an issue with the question you asked and what the agent did.
Apache-2.0. See LICENSE and NOTICE.

