gnaw (what you do to a bone) is a standalone Babashka script for browsing BONE reports. When fzf is available, gnaw lets you browse BONE reports interactively.
Assuming bbin is installed:
bbin install io.github.bzg/gnaw
Create ~/.config/gnaw/config.edn:
{:my-addresses ["you@example.com" "alias@example.com"]
:text-browser "w3m" ;; "w3m", "lynx" or "links"
:diff-pager "delta" ;; "delta", "bat" or "diff-so-fancy"
:sources [{:urls ["https://example.com/reports/all.json"]
:name "Example ML"
:repo "~/src/example"}
{:urls ["https://example.com/reports/all-open.json"
"https://example.com/reports/all-closed.json"]
:name "Another ML"}]}:my-addresses— your email address(es), used by-mto filter reports involving you. Can be a single string or a vector of strings.:text-browser— terminal browser for viewing reports onRET. When unset, gnaw probes forw3m,lynx,links, then falls back toxdg-open.:diff-pager— pager for viewing patches onC-v. When unset, gnaw probes fordelta,bat, then falls back to$PAGERorless.:sources— vector of report sources. Each source is a map with::urls— a non-empty vector of URLs/paths to BONEreports.jsonfiles whose reports are merged into one source (e.g. anall-open.jsonand anall-closed.json).:name— the source’s name. It is the unique key of a source: two sources may not share a:name, and it overrides thesourceof every report the source yields so the grouped files appear unified.--add-sourcefills it in from the siblingmeta.json’ssourcefield.:repo— (optional) local git repository of the project, or a vector of them. gnaw only lists it with-l; gnaw.el applies the source’s patches there, so it must be a local directory, not a URL.:letter— (optional) the letter identifying the source in gnaw.el’s report list, written by gnaw.el. gnaw ignores it.
:skip-columns— vector of column names to hide by default (e.g.["priority" "owner"]). Columns:mark(alias!),type,source,priority(p),deadline(d),flags,replies(#),owner,author,date,att.:report— configuration forgnaw report(see Triage report).
When adding an HTTP source, you may pass the full reports URL
(.../reports/all.json), another report file (.../reports/all-open.json),
or just the base URL (https://tracker.orgmode.org): gnaw resolves a bare
base to reports/all.json and reads reports/meta.json to record :name.
gnaw shares ~/.config/gnaw/ with gnaw.el: the configuration, the local
marks in state.edn and the cache.
gnaw [options] gnaw report [options] gnaw clear gnaw update gnaw todo gnaw prune [--dry-run]
| Option | Description |
|---|---|
-f, --file FILE | Read reports from a JSON file |
-u, --url URL | Fetch reports from a URL |
-U, --urls-file FILE | Fetch and merge from URLs listed in FILE |
-M, --my-addresses EMAILS | Your email(s), comma-separated (overrides config) |
-n, --source NAME | Filter by source name |
-p, --min-priority N | Only show reports with priority >= N (1, 2 or 3) |
-s, --min-score N | Only show reports with score >= N (0–7) |
-S, --skip-columns COLS | Columns to hide, comma-separated |
-m, --mine | Show only reports involving your address(es) |
-T, --sticky | Show only reports marked sticky |
-A, --all | Show all reports, including dismissed ones |
-t, --test-config | Verify ~/.config/gnaw/config.edn |
-v, --version | Show the gnaw version |
-h, --help | Show help |
- | Read JSON from stdin |
Closed reports never appear in the interactive list; the related-reports
view (C-/) and gnaw report still take them into account.
Two local marks help triage: sticky (Alt-*, column ! shows *) keeps
a report at hand, dismiss (Alt-Enter, column ! shows d) hides it
from the next launches, until -A shows it again. The marks are
mutually exclusive and stored with their timestamp in
~/.config/gnaw/state.edn, keyed by message-id; gnaw.el reads and sets
the same marks.
| Command | Description |
|---|---|
todo | Export the sticky reports to ~/.config/gnaw/todo.org |
prune | Drop the marks of reports no longer in any source |
prune --dry-run | List those marks without removing them |
prune needs every source in the cache: run gnaw update first.
gnaw report prints a text triage report to stdout. It accepts the
same filtering options as interactive mode (-f, -u, -m, -n, etc.).
The report includes the following sections: overview, stale patches, stale bugs, active threads, expiring reports, recent reports, and owned reports. The overview counts open and closed reports; the other sections only list open ones.
The report can be configured in config.edn under the :report key:
{:report {:sections ["overview" "stale-patches" "stale-bugs"
"active-threads" "expiring" "recent" "owned"]
:stale-days 14 ;; threshold for "stale" (default: 14)
:expiry-days 7 ;; threshold for "expiring" (default: 7)
:recent-days 7 ;; threshold for "recent" (default: 7)
:top-n 10}} ;; max items per section (default: 10):sections— list of sections to include in the report.:stale-days— number of days after which an unacked report is considered stale.:expiry-days— number of days ahead to look for expiring reports.:recent-days— number of days to look back for recent reports.:top-n— maximum number of items shown in each section but the overview.
| Command | Description |
|---|---|
-a, --add-source URL_OR_PATH | Add a reports.json source |
-r, --remove-source URL_OR_PATH | Remove a source |
-l, --list-sources | List configured sources |
Sources are stored in ~/.config/gnaw/config.edn under the :sources key.
--add-source accepts either a direct reports.json URL/path, which is
added as-is with its name read from the sibling meta.json, or a tracker
base URL (e.g. https://tracker.orgmode.org). Either way, the source
is not added when no name can be read or another source already has it.
Given a base URL, gnaw reads reports/meta.json for the source name and the
list of available report files, then lets you pick which to add (via fzf, or
a numbered prompt when fzf is absent). The chosen files are grouped into one
named source:
{:urls ["https://tracker.orgmode.org/reports/all-open.json"
"https://tracker.orgmode.org/reports/all-closed.json"]
:name "Org mode ML"}| Command | Description |
|---|---|
clear | Empty the cache, shared with gnaw.el |
update | Fetch/update reports from all sources |
By default, gnaw reads cached reports if available. If no cache
exists, it fetches from sources once and caches the result. Use gnaw
update to refresh.
| Key | Action |
|---|---|
C-n | Move to the next line |
C-p | Move to the previous line |
RET | View report in terminal browser |
C-o | Open report in system browser |
C-v | View an attachment (patch, ics, text), fetched to cache |
C-/ | Show related reports |
C-s | Change sort order |
C-r | Filter by report type |
C-b | Filter by source |
C-t | Filter by topic |
C-x | Remove current filters |
C-u | Update cache and reload |
M-* | Toggle the sticky mark |
M-RET | Toggle the dismiss mark |
C-h | Show help |
Columns: ! local mark (* sticky, d dismissed), P priority (A
high, B medium, C low), D days until the deadline (negative when
past), Flags (A)cked, (O)wned, then (C)anceled, (R)esolved, (E)xpired
or (S)uperseded, # number of replies, Att + patches, @ events,
# texts, ? awaiting a reply, ~ related reports.
Browse your reports from a local file:
gnaw -f reports.json -m
Browse all reports from a remote URL:
gnaw -u https://example.com/reports.json
Merge reports from multiple BONE instances:
gnaw -U my-urls.txt
The URLs file (-U) lists one URL per line; blank lines and #
comments are ignored.
Add a source and browse:
gnaw -a https://example.com/reports.json
Update cached reports and browse only yours:
gnaw update gnaw -m
| Path | Purpose |
|---|---|
~/.config/gnaw/config.edn | User configuration and sources |
~/.config/gnaw/state.edn | Local sticky and dismiss marks |
~/.config/gnaw/todo.org | Sticky reports, by gnaw todo |
~/.config/gnaw/cache/reports/ | Cached reports.json files |
~/.config/gnaw/cache/patches/ | Cached patches |
~/.config/gnaw/cache/events/ | Cached calendar files |
~/.config/gnaw/cache/text/ | Cached text attachments |
- Send a bug report with
[BUG] gnaw: <SHORT EXPLICIT BUG DESCRIPTION> - Send a patch with
[PATCH] gnaw: <COMMIT SUMMARY> - Send a feature request with
[FR] gnaw: <FEATURE REQUEST> - Share any other question or idea
You can also send me an email and support my work on liberapay.
This project uses Intentional Versioning, here are the three audiences:
Users: end users who browse BONE reports with gnawIntegrators: external packagersMaintainers: maintainers of the codebase
If you like Clojure(script), please consider supporting maintainers by donating to clojuriststogether.org.
Copyright © 2026 Bastien Guerry
Distributed under the Eclipse Public License 2.0.
