HomeFeaturesDocumentationContactDownload
Browse the docs

Privacy & data

Where SiteCMD stores your data, what network requests it makes, and what those requests do and don't contain.

This page is the specifics behind Why local-first. What data lives where, what gets sent over the network, and what those requests actually contain.

What stays on the execution host

Everything about your projects lives in a single per-user SQLite database, opened in WAL mode for safe concurrent reads and writes, inside SiteCMD's app-data directory:

  • macOS: ~/Library/Application Support/com.sitecmd.app/
  • Windows: %LOCALAPPDATA%\com.sitecmd.app\ (falls back to %APPDATA%)
  • Linux: $XDG_DATA_HOME/com.sitecmd.app/ (defaults to ~/.local/share/com.sitecmd.app/)

Inside that directory you'll find:

  • sitecmd.db - the SQLite database (plus sitecmd.db-wal and sitecmd.db-shm while the app is running)
  • audit.log - JSONL log of sensitive operations

Rolling application logs live in the operating system's separate per-app log directory under com.sitecmd.desktop. See Troubleshooting for each platform's path.

The database contains:

  • Scan results. Every check that ran, what it found, severity and confidence, timestamps.
  • Scan history. Every scan, retained per your prefs (default: 50 most recent per site).
  • Source-audit findings. When Code Scan runs, results land here. The source files themselves stay where they are; only the findings are persisted.
  • Project configuration. URLs, environments, linked source folder paths.
  • Issue state. Dismissals, fixes, blocked-with-reason notes.
  • Integration event data. When you connect an integration (analytics, search console, uptime), the events SiteCMD pulls are stored locally for correlation. The integration's own data still lives in the integration; SiteCMD doesn't mirror it wholesale.

What we never see

Your source code. Code Scan reads files on its execution host: your device for the desktop app, or the machine or CI runner that starts the CLI. The desktop's fix briefs are built locally and served over its local MCP server. SiteCMD does not upload source files, raw paths, or line numbers to the connected service. If you publish a CLI report, SARIF file, or CI annotation, your chosen CI provider handles that output under its own configuration. If you connect an AI editor, findings, evidence, and source excerpts returned by MCP can reach the editor's model provider under its settings. The editor can query all projects in the local SiteCMD database; project selectors do not restrict access.

Your scan results, unless you connect that site. There is no sync running behind your back: connecting a site is a thing you set up in Settings, and an installation that never does it never contacts the connected service. Once you connect one, findings for that site do reach a SiteCMD server. Connected sites below summarizes the field categories; the in-app payload inspector shows the exact serialized snapshot.

Your project configuration, unless you connect that site. Project names, environment lists, and local history stay local. A connected site's URL is registered with the service, because that is what the service is grading.

Optional local database inspection

Ordinary Code Scan does not read values from .env, .env.local, or other non-example dotenv files and does not open a project database. Source references and scrubbed example templates such as .env.example remain part of ordinary static analysis.

The scan form has a per-run Inspect local database schemas option, off by default. Enabling it permits SiteCMD to read local dotenv values only to discover a database target. It may open SQLite files inside the linked project read-only, or connect to Postgres only when every configured host and hostaddr is loopback or the connection uses a local Unix socket. Postgres catalog queries run in a read-only transaction. SiteCMD reads table, column, index, constraint, policy, and migration metadata, never application table rows. Remote and mixed-host targets are rejected. The option resets to off after the run and scheduled scans never enable it.

This inspection stays on the user's machine. It is unrelated to SiteCMD's own local sitecmd.db, does not require any globally configured database URL, and has no server-side or per-user setup step.

Network requests SiteCMD makes

A handful of outbound requests happen on purpose. Each is narrow and named.

Connected sites

Connecting a site is the one feature that sends findings to a SiteCMD server (connect.sitecmd.com), and it is off until you set it up. There is no background desktop sync loop. The app contacts that host when you perform a connected operation, while hosted schedules and deployment integrations can continue operating after you explicitly configure them.

Cloudflare D1 Time Travel keeps provider-managed point-in-time recovery data for its plan-defined window. Before each service deploy, the pipeline also writes a pre-migration database snapshot to R2 in the same Cloudflare account and deletes it after 30 days. These recovery copies are not used for normal product reads and expire under those retention controls.

The submission is a versioned snapshot. Its user-relevant field categories are:

  • The site. Its URL, and the alias you gave it.
  • Protocol and ordering. Schema version, service site and environment identifiers, submission and event sequence values, and observation and evaluation timestamps.
  • Each finding. The check that fired, its severity and confidence, and the route on your own site it fired on.
  • Each code finding. The check, plus a keyed hash of where it fired, computed under a per-project key that stays on your device, plus how many times it fired in that location. The hash is what lets the service recognise the same finding in next week's scan without ever holding anything that resolves to a file.
  • The scan itself. Engine and check-registry versions, capability-manifest digest, execution profile, stack facts, check coverage, the commit SHA the code side was observed against, and timing measurements with the route each was taken on.
  • Correlation. Privacy-preserving pairs that associate related finding groups without adding raw source locations.
  • Your decisions, once. When you first connect, the dismissals and claimed fixes you already made locally, so a site you have been triaging for months does not arrive as a fresh list of problems.

Your source code, file paths, file contents, line numbers, page HTML, response bodies, screenshots, dependency lists, lockfiles, and integration credentials have no field in that payload. The payload inspector shows the exact serialization of the snapshot it reads. A later sync rebuilds the submission from then-current local and connected state, so its sequence, deployment, or findings may differ from the earlier inspection.

When you ask SiteCMD to verify ownership, the connected service makes either one bounded GET to /.well-known/sitecmd-site-verification on that site or a TXT query for _sitecmd through cloudflare-dns.com. It reads the published challenge and does not send account identity, findings, source, or credentials.

Connecting a site also lets SiteCMD scan it from our own infrastructure, on a schedule and with the app closed. Those requests leave Cloudflare's network rather than your machine. There is no fixed SiteCMD address to allowlist and none to publish: outbound requests from Cloudflare Workers leave the anycast network, and the source address varies by the location that ran the scan. They carry the same SiteCMD/<version> (+https://sitecmd.com/scanner) identity a desktop scan sends, and the scanner page explains what is and is not provable about that.

A hosted scan also contacts cloudflare-dns.com, Cloudflare's public DNS-over-HTTPS resolver. It receives each hostname the transport scanner or isolated browser is about to fetch as an A and AAAA lookup before the request, including public subresource hosts named by the page, so an address in a blocked range is refused. For the connected site's registrable domain, it also receives the names used to query A, AAAA, SPF, DMARC, common DKIM selectors, MX, DNSKEY, CAA, and the www CNAME/address posture. It is not told which account caused the query.

Hosted scans send recognizable front-end library names and exact versions found in public script URLs to api.osv.dev, then retrieve details for any advisory IDs OSV returns. They send the registrable domain to rdap.org, which redirects to the TLD registry's public RDAP endpoint for the registration-expiry check. Neither request includes page HTML, source files, credentials, scan results, or the connected account identity.

The browser-analysis half loads each selected route in a fresh isolated browser context. Like any browser visit, it may request public scripts, stylesheets, images, fonts, and read-only fetches from hosts named by that page. SiteCMD intercepts those requests, fetches each bounded response through Cloudflare Workers public egress, and fulfills it into the browser. Target hosts may see the requested URL, request headers, and cookies the page set during that isolated load.

Scheduled scans can run this browser layer unattended while your laptop is closed, so the same subresource behavior applies even when nobody is watching the page load.

SiteCMD intercepts every browser request. It blocks unsafe request methods, service workers, WebSockets, media streams, downloads, popups, non-web schemes, and targets in blocked address ranges. The context contains none of your personal browser cookies or saved sessions and is destroyed after the route is evaluated.

Page bodies, rendered DOM, query strings, screenshots, and browser storage are never written to a SiteCMD store, log, trace, or error report. Only derived verdicts and timing measurements survive the hosted scan.

The CLI uses one site-scoped CI token for exactly four connected operations: reading the deployment ordering cursor, gate, deploy, and connected --submit. The ordering cursor contains only the site's current deployment identity, whether that credential supports GitHub OIDC attestation, and, when the credential owns the site's selected publish authority, that authority's kind, id, epoch, activation time, and current publish sequence. It contains no findings or other account data. gate sends a code-side candidate from the CI runner but persists nothing: it answers which findings are new against the site's baseline and changes nothing about what the site is known to be. deploy records deployment history, while connected --submit records or converges the deployment and its privacy-preserving code findings.

In a trusted GitHub Actions job, the CLI requests a short-lived workload identity from the runner-provided *.actions.githubusercontent.com endpoint. That request carries GitHub's runner-provided request token and the connected-service origin as the audience. It does not send source, findings, file locations, or SiteCMD credentials to GitHub. The connected service verifies the identity by fetching GitHub's public signing keys from token.actions.githubusercontent.com; that key request contains no workload identity or account data.

If you authorize a connected Vercel or Netlify deployment provider, the connected service exchanges the provider's authorization code, stores the resulting credential encrypted, and uses it only for the selected provider's project, deployment, domain, and deploy-webhook APIs. This is separate from desktop integration credentials, which remain in your OS keychain and are sent only to their owning service.

Connected alert email is delivered through api.resend.com. Resend receives the verified destination address and the transactional, alert, or digest content selected by your notification privacy mode. A connected webhook sends a signed alert or report payload to the public HTTPS URL you configured. Neither delivery includes source files, raw paths, credentials, page content, or local scan history.

What a connected site sends is aged out on a schedule, executed by a daily job on the service:

  • 90 days: the site's event stream, deployment history, and timing measurements.
  • 30 days after you disconnect a site: everything the disconnect retained, deleted outright.
  • 7 days: request receipts (the idempotency records that make retries safe).
  • One year: the erasure receipt - the one record that outlives its site, naming the site identifier and domain and nothing else, kept so you can prove the erasure happened.

A connected site's current findings and your decisions about them are kept for as long as the site stays connected: they are the baseline the service grades against, and deleting them on a timer would silently un-know what your site is.

Connected account data

The per-site records above sit under a small set of account-level records, kept so the service can tell who may act on a site and where to send what you asked for:

  • The account and its installations. Opaque identifiers, the plan tier, which installations hold admin authority, and an event log of account changes. Life of the account.
  • Alert destinations. Each email address you add, whether it has confirmed, and whether deliveries to it have bounced or been reported as spam. An address receives mail only after it confirms through a link we send it, and every alert and digest email carries a one-click unsubscribe. Kept until you delete the destination or the account.
  • Webhook URLs, webhook secret hashes, CI token hashes with their site scope, and notification settings. Life of the connected site.
  • Alert history. What was raised for a site and how each destination was notified. 90 days.
  • Provider connections. The encrypted Vercel or Netlify credential and the project and deployment identifiers it watches. Until you revoke the connection.
  • Report links. Which report each signed link opens and its expiry or revocation. Life of the site; the link itself stops working at its own expiry.
  • Account recovery requests. The request, its status, and the security notices sent to your verified destinations. Life of the account.

Alert, digest, verification, and security email is sent from alerts@sitecmd.com through Resend. Queued email content is purged once the provider gives a final answer, and the delivery record is deleted 7 days after that.

Erasing every site does not erase the account. Account erasure, which removes destinations, installations, and recovery records in one operation, and a machine-readable export of everything the service holds for an account or a site are available on request through contact.

Scans

The live-site engine fetches your URL plus a handful of related URLs (sitemap, /robots.txt, alternate URLs for security headers). Every URL SiteCMD requests passes through a network policy gate:

  • Only http:// and https:// are allowed.
  • Cloud metadata endpoints (metadata.google.internal and similar) and link-local addresses (169.254.x, fe80::) are refused everywhere, because cloud metadata services answer there and no site anyone means to scan does.
  • The target you name sets the reach. SiteCMD runs on your machine with your network, so a target you can already reach is one it can scan: localhost, a .test hostname, or a development server on your own network by its private address or hostname, including the IPv4-mapped IPv6 forms of those addresses. A hostname earns private reach only when every address it resolves to is private; a name that also answers publicly is a public site.
  • A page never earns more reach than the origin you asked for. A redirect, a stylesheet, a sitemap entry, or any other URL a fetched page names is checked against the reach of the target you named, not the page's own wishes, so a public site cannot steer a scan into your network or onto your machine. Hostnames are re-checked when the connection is made, so a DNS answer cannot change that reach after the fact.

The point of the gate is that naming an address you can already reach grants SiteCMD nothing you did not have, and nothing a scanned page says can widen it.

Local browser analysis during desktop web scans

To measure Core Web Vitals, and to run axe-core when accessibility analysis is on, SiteCMD loads the page in a hidden webview. That window is private: it shares no cookies, storage, or cache with your browser or with earlier scans, it cannot open windows or start downloads, and every top-level navigation it attempts, including redirects and meta refreshes, is re-checked against the same policy gate, so a public page cannot redirect the analyzer to an internal address. The hidden browser keeps a narrower boundary than the target you named: it loads public and localhost targets only, so a scan of an address on your own network reports Core Web Vitals and accessibility analysis as unavailable and keeps every other check.

The page's own subresources, its images, scripts, stylesheets, fonts, and fetch calls, are issued by the platform web engine to the hosts the page names. SiteCMD does not choose them, but it does fence them. On macOS and Windows it installs private-network rules in the webview before the page loads: any subresource request to a loopback, private, link-local, carrier-grade NAT, or otherwise reserved address, or to a local hostname, is blocked, and if those rules fail to install the browser layer does not run at all. A scan of your own localhost dev server still loads that server's assets. The WebRTC and WebTransport interfaces are removed from every frame before the page's scripts run, so a page cannot open a connection around the rules. What the rules cannot see is a public hostname that resolves to a private address; a request like that is in the same position it would be in your own browser. On Linux the platform webview offers no subresource filter yet, so the browser layer does not run there: Linux web scans report Core Web Vitals and accessibility results as unavailable and keep every other check.

Nothing from those requests reaches SiteCMD; the measurements stay local. Scheduled scans run this layer too, so if that matters for a particular site, run its desktop browser scans manually rather than on a schedule.

Integrations

When you connect an integration, SiteCMD talks directly to that service's API using credentials you provide:

  • Analytics: GA4 (Google), Plausible
  • Search: Google Search Console, Bing Webmaster
  • Uptime / Hosting: UptimeRobot, Cloudflare
  • Source / Deploys: GitHub
  • Performance: PageSpeed Insights
  • Issue tracking: GitHub Issues, Jira

These requests go from your machine to the integration's endpoint. They don't pass through SiteCMD servers.

Dependency lookups

The dependency update check looks up package versions in public registries depending on the ecosystems in your project: registry.npmjs.org (npm), pypi.org (Python), repo.packagist.org (PHP/Composer), crates.io (Rust), rubygems.org (Ruby), proxy.golang.org (Go), api.wordpress.org (WordPress plugins and core), and updates.drupal.org (Drupal modules). Each lookup sends only the package name in the request URL or as a query parameter. Your lockfiles and source files are never sent.

SiteCMD also queries the OSV vulnerability database (api.osv.dev) in a single batched POST request to check packages against known security advisories. The request contains package names and currently installed versions. Desktop and hosted web scans use the same database to check recognizable front-end libraries whose names and versions appear in public script URLs. Hosted requests leave SiteCMD's Cloudflare infrastructure; desktop requests leave your machine. No source file content, lockfiles, or credentials are included.

PageSpeed Insights (web performance)

Web Vitals uses Google's PageSpeed Insights service (www.googleapis.com). This call is made on demand from the desktop app - when you open the Web Vitals detail or refresh a project dashboard's performance signals - and does not run on a background schedule. The request sends the URL being measured and the strategy (mobile or desktop). If you add an optional PageSpeed API key under Settings, it is stored in your OS keychain and appended to these requests to raise Google's rate limit; without one the call is made without a key. No scan results, source file content, or project configuration are sent.

Domain checks during web scans

The web scan's domain checks look up the scanned site's public DNS records (SPF, DMARC, DKIM, MX, DNSKEY, CAA, and a CNAME/address lookup for the www subdomain). Desktop scans use your machine's configured resolver. Hosted scans use Cloudflare's public DNS-over-HTTPS resolver at cloudflare-dns.com. To read the public registration expiry date, both use rdap.org, which forwards to the TLD registry's public RDAP endpoint; the request leaves the device for a desktop scan and SiteCMD's Cloudflare infrastructure for a hosted scan. These requests contain only the relevant public DNS names or registrable domain.

License activation and validation

When you activate an existing license, SiteCMD sends your license key and a machine-scoped activation identifier to LemonSqueezy (api.lemonsqueezy.com). The activation identifier is a SHA-256 hash of your device hostname and username encoded as a 16-character hex string - your raw hostname and username are never transmitted. Ongoing validation requests send your license key and the per-activation instance ID returned at activation. We don't send anything about your projects, scans, or source.

License validation runs at app startup and periodically thereafter. If your machine is offline, SiteCMD enters an offline grace period during which connected-service and maintained-catalog credentials remain valid. Beyond the grace period those credentials may pause until the next successful validation; local product capabilities do not.

Fix-guide catalog

Eligible connected-service and existing-license entitlements receive maintained guidance as a signed catalog the app downloads in the background. Activating such an entitlement also registers the installation with the catalog service (activation.sitecmd.com), sending your license key, the same per-activation instance ID described above, and a random single-use nonce. The exchange returns an opaque access token; your license key is never sent to the catalog after that. Deactivating the license sends the same identifiers to release the installation.

The app then checks catalog.sitecmd.com about once a day. The manifest request uses the selected channel in its URL and carries the opaque token, app version, and installed catalog version. If an update is available, a follow-up pack download carries the same token and the content hash named by the manifest. Neither request includes your license key or anything about your projects, scans, or source.

Catalog packs are cryptographically signed (minisign) with a key separate from the update key, and the app verifies the signature before using a pack, so a compromised catalog server can't alter the fix guidance you see.

Auto-updates

The desktop app polls releases.sitecmd.com for new versions and downloads the update bundle from there when one's available. The polling request sends your current version and platform string. It doesn't send anything about your projects.

Update bundles are cryptographically signed (minisign). The app verifies the signature before installing, so a compromised release server can't push you a tampered binary.

Crash reports

SiteCMD does not send crash reports anywhere by default. If you opt into crash and error reports, the app sends sanitized diagnostics to Sentry (o4511662343127040.ingest.us.sentry.io). Session replay, autocapture, broad tracing, and default personal data collection are disabled. The payload is scrubbed to remove URLs, paths, emails, tokens, license keys, source snippets, request bodies, provider responses, and raw logs.

The visible preference is not the only control. The native app backend stores crash-report consent, defaults it to off, and requires native confirmation before enabling it. Renderer code cannot choose the destination or send an arbitrary Sentry body: it submits a typed diagnostic event, the backend checks consent again, sanitizes and validates the event, constructs the envelope, and uses only the baked SiteCMD Sentry ingest host.

Optional usage analytics

Usage analytics are also off by default and controlled separately from crash reports. If enabled, SiteCMD sends anonymous workflow events to telemetry.sitecmd.com, a SiteCMD-controlled Cloudflare Worker. These events help us answer product questions like which workflows are used, which scan flows fail, and what app versions are active. Each event carries the app version, build channel, operating system family, an anonymous install identifier, and your subscription tier as a plan name only ("free" for the complete local workbench, or the name of an existing subscription's legacy plan; never a license key or billing details). They do not include scan targets, project names, source code, credentials, raw logs, page content, or integration data.

Usage consent is also stored and enforced by the native backend. The renderer can request only the documented registration, event, deletion, and crash-report operations; it cannot provide a network destination or arbitrary bytes. The backend validates the exact event and property allowlists and selects the fixed SiteCMD telemetry host. Turning consent off therefore disables the transport itself, not just the Settings control.

Where credentials live

Desktop integration credentials (API keys and OAuth tokens for integrations used by the local workbench) are stored in your operating system's credential store, not in the SiteCMD database:

  • macOS: Keychain
  • Windows: Credential Manager
  • Linux: GNOME Keyring, KWallet, or any secret-tool-compatible store

The SQLite database stores references (which integration is connected, when it was last used), but never the secret itself. If someone gets your SiteCMD database file, they can't extract those tokens from it.

Connected Vercel or Netlify deployment-provider credentials are a separate opt-in. The connected service receives and encrypts that provider credential so it can read the selected deployment state and manage the deploy webhook while the desktop is unavailable. It is not copied into the local database or included in connected finding submissions.

The audit log

Every sensitive operation writes a line to audit.log in your SiteCMD storage directory. JSONL format, one line per operation. Recorded operations include:

  • License validation calls
  • OAuth flows (start, success, failure)
  • Webhook deliveries
  • Destructive data-admin operations (e.g. project deletion)
  • Panic events from the Rust backend

The audit log redacts secrets before writing. License keys, OAuth tokens, and webhook secrets are reduced to a key prefix and a hash. Email addresses are reduced to a domain. Hostnames are kept; raw URLs with query strings are not.

You can read the audit log at any time. SiteCMD does not upload it.

Local data export

SiteCMD provides these local exports:

  • Full SQLite database backup from Settings → Data.
  • Issues CSV for filtered or complete issue lists.
  • Activity CSV or JSON from the Activity page.
  • Report PDF or HTML for stakeholder-facing snapshots.

SiteCMD writes each exported file to the path you choose. The database stores report history metadata, not the contents of those exported files. A database backup does not include OS keychain credentials or connected-service state. Contact us for a broader account export request.

Local and connected data deletion

To wipe a single project, delete it from Settings → Site Setup. SiteCMD removes the project's scan history, findings, integration data, and any associated keychain entries.

To wipe all local SiteCMD data, quit SiteCMD and delete the storage directory listed at the top of this page. Integration credentials in the OS keychain are separate; search for "SiteCMD" entries in Keychain Access (macOS), Credential Manager (Windows), or secret-tool (Linux) and remove them.

Deleting local data does not erase a connected site's remote state. Use Settings → Connected → Erase Site Data for immediate remote erasure and retain the receipt the app shows once. Stop Watching This Site starts the connected service's 30-day retention period. Unlink This Desktop only removes the local connection; it does not stop or erase the hosted site.

What we'd do if compelled

We cannot provide local source, scan history, integration data, or project records that never reached our service. We can be required to provide records we do hold, including billing and license records, optional uploaded telemetry, contact records, and the connected-site data described above. We would respond only to a legally valid request and within its scope.

Reporting a security issue

If you find a security issue in SiteCMD itself, see Security disclosure.