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. Full list is in manifest.json on GitHub; 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
Access the current tab only when you invoke Makro. Powers text expansion and the search overlay.
read your browsing history, inspect other tabs, or modify pages in the background.
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 automatic backups at your chosen interval - daily, weekly, or monthly.
wake up for any reason other than a scheduled backup, run background fetches, 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 removes tracking iframes from pages where Makro operates. offscreen runs the screenshot-OCR pipeline in a sandboxed document - no network.
inspect or modify arbitrary network traffic, or run persistent background fetches.
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.

2,492
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 that every telemetry endpoint sends. No macro content, no AI prompts or responses, no browsing history. The extension has two analytics toggles in Settings; both default to off, which means the weekly heartbeat sends nothing at all.

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 hashRandom 32 bytes generated at first install, hashed · lets us count unique installs without a raw device id

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, ONLY if analytics is enabled in Settingsbackground.js

Default: both toggles off, zero data sent. The heartbeat function exits at the first line if neither analyticsBasicEnabled nor analyticsDetailedEnabled is true. If you opt in, the payload is built in three additive tiers.

Base fields (any analytics toggle on)
deviceHashSHA-256 hashSame hash as /api/install · lets us dedup weekly actives
version, platform, localeas aboveSame meanings as the install payload
tier"free" / "pro" / "premium"From settings.tier; never contains the license key itself
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
timeSavedSecondsintegerDerived as expansionCount * 3.5 seconds
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 seven settings are on: clipboard, localAiOnly, autoBackup, spellCheck, animation, top10, repeatLast · no counts, no timing, no macro-level data
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