Security & architecture

The math, not the marketing.

Privacy claims are only as good as the cryptography behind them. Here's exactly how Makro encrypts your macros, what our servers can and can't see, the attacks we've designed for, and the evidence we publish. Every parameter is listed. Every edge case is named.

Encryption
AES-256-GCM
128-bit auth tag · 96-bit IV
Key derivation
PBKDF2-SHA256
600,000 iterations
Architecture
Zero knowledge
Server sees ciphertext only
Free tier content
Never leaves device
Macro content stays on your device · cloud AI is opt-in · anonymous usage telemetry is documented below
01 / Crypto

What actually runs, when you save a macro.

Every macro gets its own IV, its own ciphertext, its own authentication tag. The device key is non-extractable - even with full access to the browser profile, an attacker can't pull it out as raw bytes. Here are the parameters, byte-for-byte.

Encryption parameters crypto.js
AlgorithmAES-256-GCMAuthenticated encryption · integrity-verified on decrypt
Key size256 bitKeys generated via crypto.getRandomValues(Uint8Array(32))
Auth tag128 bitTampering detected on decrypt · GCM authenticated
IV96 bit randomFresh per macro · crypto.getRandomValues
Key derivationPBKDF2-SHA256600,000 iterations · exceeds OWASP 2025 minimum
AAD bindingid + flagsAAD is a JSON object: {id, confirmBeforeExpand, copyToClipboard} · these unencrypted fields are bound into the auth tag so tampering or cross-record ciphertext swaps fail decryption
Derived keyextractable=falseThe AES-GCM CryptoKey produced by PBKDF2 is created with extractable=false and cannot be exported. The underlying seed the key is derived from is a separate consideration, covered in the threat model below.
Web Crypto API crypto.js
// Derive per-user key from passphrase locally. // Runs in your browser. The passphrase never // leaves this device; neither does the key. const material = await crypto.subtle.importKey( "raw", encoder.encode(passphrase), { name: "PBKDF2" }, false, // non-extractable ["deriveKey"] ); const key = await crypto.subtle.deriveKey( { name: "PBKDF2", salt: deviceSalt, // 256-bit random iterations: 600_000, hash: "SHA-256" }, material, { name: "AES-GCM", length: 256 }, false, // extractable = false ["encrypt", "decrypt"] );
02 / Zero-knowledge

What you see. What we see.

Your macros exist in readable form only on your devices. Before they touch our sync infrastructure, they're encrypted with a key we don't have. Our servers store blobs that would take cosmological timescales to brute force.

On your deviceReadable
plaintext · memory-only · decrypted with local key
.sig
Email signatureDenver · makroexpander.com · +41...
.thx
Quick thanks replyThanks so much - I really appreciate...
.meet
Meeting linkhttps://cal.com/denver
.eta
ETA responseI'll have this ready by Thursday EOD...
encrypt
On our serversCiphertext
opaque blobs · no keys · no metadata beyond size
blob.01
8f3a92c1e047b6d5f21a0e9c3b6e82f51d0a8c3b...
blob.02
c4d7e2f1a0b9c8d3e6f5a4b3c2d1e0f9a8b7c6d5...
blob.03
1e2d3c4b5a697886957431eae8c7b6a54f3e2d1c...
blob.04
7b6a5948372615043f2e1d0c9b8a79685746352c...
If our servers were compromised tomorrow, an attacker would download a pile of ciphertext with no key material. Brute-forcing one 256-bit AES key exhausts the age of the universe. Zero-knowledge isn't a claim - it's an architectural consequence.
03 / Comparison

How Makro differs, by default.

Most competitors encrypt on their servers and hold the keys. We encrypt on your device and don't. Here's how the standard text expanders stack up on the security questions that actually matter.

Security property
Makro
TextExpander
Text Blaze
Espanso
Encryption at restLocal-storage macros encrypted before written
AES-256-GCM
AES-256server holds keys
AES-256server holds keys
-local files
End-to-end encrypted syncProvider can decrypt your cloud content?
Zero-knowledge
Not E2E
Not E2E
-no sync
Key derivationPBKDF2 / Argon2 with disclosed params
PBKDF2 600k
Not disclosed
Not disclosed
N/A
No account requiredInstall without email, phone, payment
Yes
Signup required
Signup required
Yes
Local AI optionAI rewrite that never touches a network
Ollama, LM Studio
No
No
No
Local-only settingsToggles to force local AI + auto-clear sensitive clipboard
Built-in
No
No
No
Can provider read your data?Compelled disclosure, subpoena, breach
Mathematically no
Yes
Yes
Local only
Full comparison on larger screens.
04 / Threats

What happens when things go wrong.

Every threat model lives or dies by specificity. Here are four realistic compromise scenarios and what Makro's architecture does in each one. Spoiler: your macros stay unreadable in all four.

THREAT 01

Your laptop gets stolen.

An attacker boots from USB and images your drive, or pulls your browser profile directly from disk.
They see ciphertext, plus the device key seed. All macros are encrypted at rest with AES-256-GCM. The device key seed and salt live in chrome.storage.local alongside the ciphertext, so at-rest encryption protects against raw disk-level access to browser LevelDB files, other extensions reading the storage partition, and casual inspection of storage exports. It does not protect against an attacker with direct runtime access to this extension's storage (same-user malware or an unlocked browser). True zero-knowledge at rest would require a passphrase on every browser startup, which we do not currently require. Cloud sync is a different story and is zero-knowledge - see threat 02.
Encryption at rest · every macro, fresh IV per record
Non-extractable CryptoKey · the derived AES-GCM key cannot be exported
Documented in crypto.js · the BG-04 note in the file header names this limitation explicitly
THREAT 02

Our servers get breached.

A zero-day gives an attacker full database access to Makro's sync infrastructure, or a rogue employee dumps the blob store.
They download ciphertext with no keys. Zero-knowledge means we never had the keys to steal. Even with full read access to every byte we store, an attacker can't decrypt a single macro - the math forbids it.
Zero-knowledge storage · encryption keys never reach the server
Payload pre-encrypted · AES-256-GCM before HTTPS upload
Per-user sync keys · derived locally per license, no cross-account reuse
THREAT 03

A government comes knocking.

A subpoena, warrant, or national security letter compels Makro to hand over your data.
We hand over what we have. That's encrypted blobs we cannot decrypt, billing records, and the fact that an account exists. Your macro content is cryptographically unavailable to us - we cannot produce it because we never had it.
No passphrase escrow · zero recovery backdoor
Minimal metadata · only billing + install counts
No macro content · stored as ciphertext we cannot decrypt
THREAT 04

Someone intercepts your sync traffic.

A hostile network (coffee-shop WiFi, malicious ISP, compromised proxy) sniffs every packet between you and our servers.
They see double-encrypted nothing. Sync traffic is encrypted via HTTPS/TLS in transit, and the payload inside is already AES-256-GCM ciphertext. Intercepting the connection yields random-looking bytes they still can't decrypt.
TLS 1.3 · modern transport encryption
Payload pre-encrypted · belt-and-braces
AAD binding · rejects replay/substitution
05 / Permissions

Every permission, one purpose.

Makro declares the permissions below in its Chrome and Firefox manifests. Each has a specific job. Every permission and host, with its purpose, is itemized in the privacy policy; the ones users actually notice are detailed here.

storage
Stores your encrypted macros, categories, and settings in browser-managed local storage.
transmit storage contents anywhere, share across extensions, or persist data after uninstall.
activeTab
Act on the tab you are looking at when you invoke Makro from the toolbar or the right-click menu. Declared so Makro never has to request the broader tabs permission.
by itself grant access to any other tab. Note that the all-sites host access below is a separate, broader grant, and it is the one that governs what Makro can reach.
clipboardRead, clipboardWrite
Powers clipboard history and paste-back. Read requires the extension to be foreground; write is used when you explicitly copy from a macro.
read clipboard contents in the background, upload clipboard to any server, or store clipboard outside your device.
alarms
Schedules seven fixed timers: automatic backup at your chosen interval, license revalidation and cloud sync pull every 6 hours, the weekly anonymous heartbeat, session auto-lock, Teams membership refresh, and OCR worker idle shutdown.
wake for anything outside that list, read page content on a timer, or track idle time.
contextMenus, notifications, downloads
Browser UX surfaces: right-click menu entries, import/export file prompts, and the occasional update notice.
send push notifications you didn't trigger, or download files without your consent.
declarativeNetRequest, offscreen
declarativeNetRequest carries exactly two static rules (rules.json), which rewrite the Origin header on requests to a local Ollama server, one for localhost:11434 and one for 127.0.0.1:11434, so Ollama's CORS check accepts them. offscreen runs the screenshot-OCR pipeline in a sandboxed document, Chrome only, no network.
read, block, redirect, or modify any request other than that local Ollama header rewrite.
host access (all sites), scripting
Expansion has to work in whatever text field you are typing in, so Makro's content script matches every site. On Chrome that access is granted at install. On Firefox it is optional: Makro ships without it and asks inside the extension, and until you grant it expansion runs only where you allow it. scripting re-injects that content script into tabs that were already open when Makro installed or updated, so you do not have to reload the page.
read, log, or transmit what is on the pages it runs on. It watches locally for your own hotwords and writes your own macro text back.
Local-only
When your data cannot leave the device.
For healthcare, legal, financial, or any context where data must stay local, two settings keep it there. They sit next to each other in Settings and can be toggled independently. Local AI only defaults to on for every fresh install, so cloud AI is never used until you explicitly opt in.
x
Local AI only - cloud Smart Rewrite blocked at both UI and network layersThe setting is checked in background.js (four call sites) and ai.js before a request is formed. Default on for new installs.
x
Clipboard auto-clear - sensitive clipboard entries never persistTriggered when clipboard content originates from a password field or matches a sensitive pattern
+
Macro expansion - fully functionalAll your macros work normally, everything stays local
06 / Audits

Every feature ships with an audit.

Security reviews aren't quarterly; they happen inside the development cycle. Before anything reaches production, it passes through a multi-layer review process - automated tests, AI-assisted code review from an independent model, and human sign-off on encryption-touching changes.

7,133
Automated tests
Unit + integration tests covering extension, worker, and encryption code · run on every change
Pre-commit
Lint + scan gates
Security-focused static analysis and scanner chain applied before a change lands
Release
Regression suite
Behavioural checks that revisit previously-fixed classes of bug before each release
Multi-layer
Review discipline
Automated scans plus review-time scanners plus an extra audit pass on substantial changes

A chain of security scans, not one tool.

Security scanning runs at three levels: automated gates on every change, additional scanners as part of our review discipline, and policy commitments that kick in for substantial changes. The categories below are labelled so you can tell what's machine-enforced versus what's run as team practice.

Automated checks on every pull request. Our CI pipeline runs static analysis with a security-focused lint ruleset, a full unit-test suite, an extension-store linter, a clean release build, and a version-sync check across the extension manifests. Failures are visible on the PR and are resolved before merge.

Review-time scanner chain. On top of CI, our review workflow runs secrets scanning, dependency vulnerability checks, SQL-safety audits across database queries, and locale-parity checks across all seven supported languages. A behavioural regression suite revisits previously-fixed classes of bug before every release.

Substantial-change policy. Larger diffs get a second independent audit pass in addition to everything above. This is a policy commitment, not an automated build step; we keep it deliberately small on purpose so the reviewer has room to look at design, not only syntax.

READ ARCHITECTURE DOCUMENT
07 / Telemetry

What we do collect, stated plainly.

Here is every field the install ping and the weekly heartbeat send. No macro content, no AI prompts or responses, no browsing history. The extension has two analytics toggles in Settings and both default to off. With them off the weekly heartbeat still sends a small anonymous base payload, listed below; the toggles control everything beyond it.

POST /api/install · once, on fresh install onlybackground.js
versionextension versione.g. "1.4.8"
platformOS familyFrom navigator.userAgentData.platform · e.g. "macOS"
localebrowser localee.g. "en-US"
deviceHashSHA-256 hash, only if you opted inRandom 32 bytes generated at first install, hashed · lets us count unique installs without a raw device id. Added ONLY when analyticsBasicEnabled is on; a default install sends no device identifier at all

This is the only telemetry that is not opt-in. It fires once, on chrome.runtime.onInstalled with reason "install". It does not fire on update. Uninstalling is the only way to prevent it. (An internal isDev flag is also attached; it is always false for builds shipped from the Chrome Web Store or Firefox AMO and is only used to exclude our own development installs from aggregate counts.)

POST /api/heartbeat · weekly · anonymous base payload always, more only if you opt inbackground.js

Default: both toggles off, anonymous base payload only. The heartbeat always sends the five base fields below. They carry no device identifier and no usage data, so they cannot be tied back to you. Everything past that is additive and gated on the two toggles. On Firefox this base payload is governed by the browser's own technical-data consent instead of an in-product toggle: Firefox 140 and later show that choice at install and declining it stops the heartbeat entirely, while on Firefox 128 to 139, which predates the prompt, Makro cannot confirm a choice was offered and does not send it at all.

Base fields (no in-product toggle; see the Firefox note below)
version, platform, localeas aboveSame meanings as the install payload
tier"free" / "pro" / "premium"From settings.tier; never contains the license key itself
isDevtrue / falseAlways false in store builds; lets us exclude our own development installs from counts
popupSurface"window" / "tab"Which of the two UI surfaces this install actually uses, so we can decide whether both are worth keeping. A UI flag, not a usage count · temporary, removed once that decision is made
deviceHashNOT in the base payloadAdded only when analyticsBasicEnabled is on. Without it the base payload has no device identifier, so weekly actives are counted by heartbeat volume alone
Basic analytics (analyticsBasicEnabled = true)
expansionCountintegerTotal text expansions since install · no per-macro breakdown
macroCountintegerHow many macros you have defined · no names or content
categoryCountintegerHow many categories you have
timeSavedSecondsintegerMeasured from tracked expansion length at 200 characters per minute, falling back to a flat 3.5 seconds per expansion for usage from before that tracking existed, or if the tracker cannot be read
daysSinceInstallintegerCalculated locally from the install timestamp
Detailed analytics (analyticsDetailedEnabled = true)
browserbrand stringFrom navigator.userAgentData.brands[0].brand · e.g. "Chrome"
aiProvider"ollama" / "lmstudio" / "cloud"Which AI backend is selected in Settings
theme"dark" / "light"Which appearance setting you are using
importSourcelast import toolBlank unless you imported from a competitor (e.g. "textexpander")
featuresUsedJSON array of flagsWhich of these five settings are on: clipboard, localAiOnly, autoBackup, top10, repeatLast. The same array also carries moment:<key> entries recording which free-tier click opened an upgrade panel, repeated once per occurrence and capped at 50 per key and 150 overall, so it does encode counts. No timing, no macro-level data
creditUsageBreakdowncurrent-month counts per AI serviceHow many rewrite, translate, OCR and vision calls you made this month. Counts only, no prompts or results. Current month only; history stays on your device
betaUsagecurrent-month Page Variables countsHow many Page Variables you captured and filled this month. Counts only, and only while the beta is on
settingsNonDefaultlist of setting namesWhich settings you changed away from their default, by NAME only, capped at 64. Never the values you chose
canarycountersCounts from whichever feature is mid-rollout, so we can tell whether it is failing before ramping it further. Counts only
POST /api/event · CTA clicks and form submits on the website only (never from the extension)shared.js eventTrackingScript

Two event shapes share this endpoint. Every request includes name and page; the data object differs by event type.

Top-level fields (every event)
nameevent nameSee how the name is chosen per event type below · there are no other possible values
pagepathnamee.g. "/security"
Click events (the handler matches .cta-btn, .nav-cta, [data-event])
name (source)data-event attribute if set, else href sniffIf the clicked element has a data-event attribute, its exact value becomes the event name. Otherwise the name is derived from the href: contains "chromewebstore.google.com" yields cws_click, contains "addons.mozilla.org" yields amo_click, anything else yields cta_click. The data-event attribute also serves as a way to opt a non-CTA element into tracking.
data.textfirst 50 charsTruncated visible text of the clicked element, trimmed
data.hrefhref attributeThe destination URL of the clicked link, or empty string if the element has no href
Form submit events (the handler matches every <form>)
name (source)always "form_submit"Hardcoded in the submit handler · not overridable by data-event
data.actionform action URLFrom the form's action attribute; falls back to the current pathname if the form does not set one. No form field values, no names, no file content are included.
POST /api/uninstall-feedback · only if you fill out the uninstall surveybackground.js chrome.runtime.setUninstallURL
Nothing automaticUser-initiated onlyThe uninstall page contains a short opt-in form; submitting it sends your freetext reason. Declining or closing the tab sends nothing.

That is the complete list of endpoints the extension and the website call with any user-adjacent data. No pixels, no third-party analytics SDKs, no session replay. AI usage metadata (token counts, endpoint, model, cost) is retained for up to 90 days for billing; install and heartbeat aggregates are retained for cohort analysis.

08 / Negative space

What Makro never does.

Trust is often built as much by the things a product refuses to do as by what it chooses to build. Each item is either technically impossible given our design, or an active operational commitment.

Log your AI prompts or responsesContent is transient · only usage metadata (token counts, timestamp) is retained, for up to 90 days, for billing
Train models on your dataNever · neither Makro nor the underlying Cloudflare Workers AI inference runs fine-tuning on your content
Sell data to third partiesWe have no advertisers, no data partners, no data to sell
Show adsIn any product surface, free or paid
Track you across sitesNo pixels, no fingerprints, no cross-origin reads
Share with advertisersWe have none · the business model is paid subscriptions
Require an account for the free tierInstall works with zero personal data collected · Pro activation uses a license hash, never the raw key
Hold your license key in plaintextLicense is SHA-256 hashed before any network call · no escrow, no recovery backdoor
Sync without encryptionEvery sync payload is AES-256-GCM ciphertext, then HTTPS on top

Private by math, not by policy.

Zero-knowledge · AES-256-GCM · no account required · free forever