Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Release notes
Changelog
Elyra
Security & privacy

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.com for 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, localhost and .local — are refused on every redirect hop, on sitemap URLs named by a site's robots.txt or 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.1 is 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:3000 or 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.