# Privacy and security [← README](../README.md) The plugin runs unsandboxed inside `omarchy-shell`, like every Omarchy plugin. That is worth taking seriously, so here is exactly what it does. **It talks to one host.** `https://www.cnrtl.fr`, and only for `/api/word/…` — plus `/api/search/…` if you switch `onlineSearch` on. No analytics, no fonts, no images: the quotation rules and the usage sparklines are drawn, not downloaded. `lib/Portal.js` re-derives the host from every finished URL and refuses anything else, and redirects are not followed, so an allowlisted host cannot hand the request onward. **Nothing you type is sent, by default.** A query is resolved against the bundled index in `data/words/`. A form that reaches the network came from that index, from the portal's own answer, or from a word printed inside an article the portal served. What you type is never part of a request, which is why there is nothing for anything to be injected into — and why search still works offline. With `onlineSearch` on, the query itself goes to the portal once the local index has found nothing, bounded to 40 characters and held to the same character allowlist. **Every variable part of every URL is checked character by character.** Not by a pattern that might have a range in it by accident, but by a scan against an explicit allowlist: letters, the two ligatures, and four punctuation marks that appear in TLFi headwords. The part of speech comes from a fixed list, the route from a fixed list, the dictionary from a fixed list. **No shell is ever invoked.** Every request is an argv array handed to `curl` — there is no `sh -c`, so there is no quoting to get wrong. Requests are bounded in time and in bytes, and compression is off, so a response cannot expand into the shell's memory. **Nothing the portal sends is rendered as it arrived.** `lib/Article.js` parses the markup into a closed set of known constructs and writes the displayed markup itself, so a tag it does not know cannot reach Qt's rich-text engine. Scripts, images, styles and third-party anchors are dropped with their attributes; the text inside them survives, escaped. A cross-reference becomes a link only after the form it points at has been validated, and the link the panel acts on is rebuilt here rather than copied from the page. `lib/Sections.js` type-checks every field of the JSON — every string bounded, every list capped, every number clamped — before any of it becomes a list model. **Two URLs are opened in your browser, and only two shapes.** The source line hands the portal's own page URL to `xdg-open`; the Proxémie tab opens ATILF's graph at `proxemie.atilf.fr`. Both are rebuilt from validated parts rather than taken from the page. ## What it writes Under `~/.local/state/omarchy/plugins/jmaeder.frenchdict-cnrtl/`, and nowhere else: | | | |---|---| | `entries/` | Cached entries, one file per word and part of speech, named from the key's bytes in hex — so a file name is `[0-9a-f]+.html` by construction, and no word can steer where a file lands | | `cache-order.txt` | Which entry was read when, for the size cap | | `state.json` | The words you have kept, your recent lookups, and which tab you were on | Both lists are re-validated on the way back in: that file is editable by anything running as you, and nothing read out of it reaches a URL without passing the same allowlist a fresh lookup does. ## What it reads, and how much of it Four files are read while the plugin runs: one index shard from `data/words/`, one cached entry, `cache-order.txt`, and `state.json`. All four live under your home directory, so all four are writable by anything running as you — and between the moment the plugin writes one and the moment it reads it back, a file can have grown, been replaced, or been turned into a symlink pointing at something that never ends. So no file is read whole and asked about afterwards. `lib/Files.js` builds the one command every read goes through, and it is a ceiling three times over: `find` refuses anything that is not a regular file — a symlink, a directory, a fifo, a device — and refuses a file that has reached the cap, before anything is opened; `head` stops at the cap anyway, which covers the file growing between the two; `timeout` stops a read the filesystem has stopped answering. A file that fails any of the three produces nothing, which every caller already treats exactly as it treats a file that was never there. | | | |---|---| | An index shard | 4 MB, against a largest shipped shard of 120 kB | | A cached entry | 2 MB, the same ceiling that decides what gets cached in the first place | | `cache-order.txt` | The entry cap times the longest name it can hold | | `state.json` | 256 kB, against about 60 kB of history and kept words at their limits | The caps in `lib/Cache.js`, `lib/Store.js` and `lib/Index.js` still apply afterwards. They are the second line now rather than the first: what they check is a string whose size was settled before it existed. Writes happen immediately rather than on a timer, so a shell that dies does not lose what you kept. `omarchy plugin remove` leaves that directory behind. Delete it by hand to remove the reading history, or middle-click the bar icon to empty just the entry cache.