Security & privacy
Local-first
Projects, analyses, crawls, monitors, and history are stored locally in
SQLite, under ~/Library/Application Support/Elyra Sja/. Nothing is sent
anywhere except:
- the pages/sites you analyze (fetched directly),
- Google PageSpeed Insights, if you enable Performance,
- Google (accounts + Search Console API), if you connect Search Console,
- your chosen AI provider, when you use Run with AI,
- the answer engines you enable for the citation probe (OpenAI, Anthropic, Perplexity), which receive the question you type — the page URL is not sent; the engine finds or fails to find the page through its own web search,
- the crawler operators' published address lists (openai.com, developers.google.com, bing.com, perplexity.com, apple.com) when you verify a log's visitors; the lists are downloaded and cached under Application Support for a week, and the comparison is local — no address from your log is sent,
elyracode.comfor update checks.
Secrets in the Keychain
The AI API key, the citation probe keys (OpenAI, Anthropic, Perplexity),
the PageSpeed API key, the Search Console OAuth client secret and
refresh token, and the license code are stored in the macOS Keychain
(service com.elyra.sja), never in the SQLite database. On save they are written to the Keychain and blanked in the DB; on load
they are read back from the Keychain. A legacy value found in the DB is migrated
to the Keychain on the next save.
If the Keychain refuses a write, the value is kept in the database instead of being dropped, and the app says so. A failed write used to be discarded silently, which meant an activated license could be gone after a restart.
Search Console OAuth
The OAuth client is yours, not ours: the app ships no Google credentials, so
there is no non-confidential secret in the binary to extract. The flow is the
installed-app one Google recommends — PKCE (S256) with a redirect to a
temporary 127.0.0.1 port that is bound only for the length of the flow, plus a
random state that must match or the callback is refused. The scope is
read-only (webmasters.readonly); Sjá cannot change anything in your Search
Console. The refresh token goes to the Keychain, the short-lived access token
is held in memory only, and a revoked grant is discarded so the app falls back to
"not connected" instead of retrying a dead token.
With per-project connections enabled, each project's refresh token is stored
under its own Keychain account (gsc_refresh_token_p<id>), so one client's Google
access is never reachable through another project.
Data retention
History is capped at startup so the database can't grow without bound: the 20
newest runs per URL and the 20 newest crawls per project are always kept, older
ones past 90 days are removed, alerts are capped at 500, and space is reclaimed
(VACUUM) only when there is a meaningful amount free.
Fetching pages
Sjá follows URLs you give it, including redirects, and fetches URLs it discovers along the way (a site's sitemap, for one). Without care, a hostile page could redirect the app at a router admin panel and use it to probe your LAN. Where a request may go is guarded in three layers:
- Address literals in a private or local range — loopback, private ranges,
link-local (including
169.254.169.254), CGNAT, IPv6 unique-local, IPv4-mapped equivalents,localhostand.local— are refused on every redirect hop, on sitemap URLs named by a site'srobots.txtor a sitemap index (which must also be on the site being crawled), and on MCP tool arguments. - Hostnames go through a filtering DNS resolver that drops answers in those
ranges, so a name that happens to resolve to
10.0.0.1is refused at connect time. A literal check alone is bypassed by exactly that. - What you type is exempt. A URL you enter yourself — in the launcher, as a
crawl seed, as a monitor, as a project's site URL, or on the CLI — may point at
http://localhost:3000or an intranet staging host: auditing your own dev server is a legitimate workflow, and you are the one asking. Only that host is exempt; a redirect from it to a different private name is still refused.
The MCP server does not get the exemption. Its URLs come from an AI assistant,
which may itself have been steered by a page it just read, so local and private
targets are refused there unless the server is started with
ELYRA_SJA_ALLOW_PRIVATE=1 — the explicit opt-in for auditing a local dev server
through an assistant.
Response bodies are capped at 5 MB and decoded by their declared charset.
The AI file sandbox
When the AI agent edits code, its file tools (list_dir, read_file,
write_file) are sandboxed to the project's local code path. Paths are
canonicalized — which resolves symlinks — and rejected if they land outside the
root, so a symlink inside the project cannot be used to reach e.g. ~/.ssh.
A dangling symlink (one whose target does not exist yet) is rejected outright:
it used to slip through the check and let a write follow it out of the project.
.git, node_modules, target, and dist are skipped when listing. With
Ask before the AI writes files enabled (default), every write pauses for a
preview and explicit approval, and times out rather than waiting forever.
Writes are atomic: the new contents go to a temporary file beside the target, which is then renamed over it. A write that dies halfway — the app quits, the disk fills, the model's turn is cut off — leaves your file as it was rather than half-overwritten. An existing file keeps its permissions.
The page being fixed is third-party content, and its title, headings and URL
end up in the prompt that drives an agent with write access to your checkout. Fix
prompts therefore quote every value taken from the page between « and »,
stripped of control characters and capped in length, and the agent's system
instructions say to treat that text strictly as data and never to follow
instructions found inside it. That narrows the blast radius of a page that tries
to talk to the model; the write confirmation remains the hard stop.
There is currently no in-app undo for AI edits — keep the project under version control (git) so you can review and revert diffs.
Updates
Auto-updates are ed25519-verified: the downloaded artifact must verify against the public key embedded in the app before it is applied, so a compromised release host still cannot push a malicious binary.
Distribution integrity
Release builds are signed with a Developer ID certificate, run with the hardened runtime, and notarized + stapled by Apple — so Gatekeeper verifies them offline. Releases include SHA-256 checksums.
Exported reports
PDF/HTML exports can contain client data, so they are written to an owner-only
(0700) directory under Application Support — not the shared /tmp — and pruned
after a day.
Reporting
Do not put API keys, license codes, private URLs, or client data in public issues. Report security concerns privately to the maintainer.