OrgDrift is built on a simple principle: we should see as little of your data as possible, and what we do see should be protected immediately. This page explains exactly how that works.
Last updated: September 8, 2026
The short version
[REDACTED] before any processing begins.OrgDrift detects misalignment between HR systems, CRM platforms, and sales compensation tools. You upload CSV exports from these systems. Our engine compares them, identifies drift, and generates a Control Execution Record you can act on and use for audit evidence.
The scan engine runs entirely in your browser using JavaScript. When you upload a file, it is read into browser memory using the FileReader API. It is not transmitted to any server. Not now, not ever during the scan.
Contact and assessment requests send the details you enter. Other account, payment, and optional-feature metadata are described below and on the security page. A request form does not send your scan results.
The moment your CSV is parsed, the engine identifies PII-bearing columns — full name, first name, last name, email, and manager name — and replaces every value in those columns with [REDACTED] before any comparison or analysis runs. This happens synchronously, as the first step inside parseCSV().
The fields treated as PII are derived from the OrgDrift Trust & Data Governance Framework classifier — every field categorized as an Optional Identifier is stripped:
// Derived from the governance classifier — every OPTIONAL_IDENTIFIER FieldType.
export const PII_FIELDS: FieldType[] = [...fieldsInCategory('OPTIONAL_IDENTIFIER')]
// Currently: FULL_NAME, FIRST_NAME, LAST_NAME, EMAIL, MANAGER_NAMEThe stripping function runs against every row before any state is set:
function stripPII(
rows: Record<string, string>[],
fieldMap: Partial<Record<FieldType, string>>,
): Record<string, string>[] {
const piiColumns = PII_FIELDS
.map(f => fieldMap[f])
.filter((col): col is string => Boolean(col))
if (piiColumns.length === 0) return rows
return rows.map(row => {
const clean = { ...row }
piiColumns.forEach(col => { clean[col] = '[REDACTED]' })
return clean
})
}The result: by the time a row reaches any comparison, cohort analysis, or drift check function, it contains no employee names and no email addresses. The engine never sees them. They exist only in browser memory for the milliseconds it takes to parse and strip, then they are gone.
Employee IDs are the key used to match records across systems (e.g., matching an HRIS record to an ICM record). We need to compare them — but we do not need to store the originals.
Before any cross-system comparison, every employee ID is passed through SHA-256, the same cryptographic hash function used by SSL certificates and most modern authentication systems. SHA-256 is a one-way function: given a hash, it is computationally infeasible to recover the original value. Two records with the same employee ID will produce the same hash — so comparisons still work — but the raw ID is never retained.
This runs natively in the browser via the Web Crypto API — no library, no external call:
/**
* SHA-256 via Web Crypto API — no library, works in every modern browser.
* One-way — cannot be reversed. Employee IDs are hashed before any comparison.
*/
export async function hashId(rawId: string): Promise<string> {
const normalized = rawId.trim().toLowerCase()
const encoder = new TextEncoder()
const data = encoder.encode(normalized)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
}For datasets, every row is hashed in parallel before processing:
export async function hashDataset(
rows: Record<string, string>[],
idColumn: string,
): Promise<{ rows: Record<string, string>[]; idMap: Map<string, string> }> {
const idMap = new Map<string, string>()
const hashed = await Promise.all(rows.map(async row => {
const rawId = row[idColumn] ?? ''
let hashedId = idMap.get(rawId)
if (!hashedId) {
hashedId = await hashId(rawId)
idMap.set(rawId, hashedId)
}
return { ...row, [idColumn]: hashedId }
}))
return { rows: hashed, idMap }
}The idMap (original → hash) exists only in local function scope during the analysis run and is garbage-collected when the function returns. It is never written to localStorage, sessionStorage, or any network destination.
For full transparency, here is the exact sequence every file goes through:
Almost nothing.
We do not use remarketing pixels. We do not sell or share your data with advertising networks.
We use Microsoft Clarity for session replay on our marketing pages, and nowhere else — never on the scan workspace or any page that can display your data. See Section 7 for what it captures and how to opt out.
One thing does leave, and we would rather tell you than have you find it. When you map columns, the column header names from your file (for example EE_ID, GRS_PAY) are sent to our mapping service, which asks a language model what each header probably means. Header names only — never a row, never a cell value, never an employee record. If you would rather send nothing at all, turn on Private Mode and column mapping runs entirely in your browser. See Section 7.
OrgDrift uses a minimal set of third-party infrastructure:
Session replay never runs where your data is. Clarity records mouse movement, clicks, scrolls, and a reconstructed view of the rendered page. That is fine on a marketing page and unacceptable on a page showing payroll records, so we do not load it on one. The scan workspace, the verification console, findings, remediation, and governance surfaces load no session-replay script at all — not a masked one, none. You can confirm this yourself: open any of those pages, view source, and search for clarity. As a second line of defense those routes are also wrapped in data-clarity-mask, and their Content-Security-Policy header permits no third-party connections whatsoever.
Private Mode. Append ?private=1 to any scan URL. Column mapping then runs entirely in your browser, the mapping service is never called, and no analytics or replay script loads. The scan page makes no request carrying any part of your file. To verify: load the page, disconnect your network, and run a scan. It completes.
How to opt out. You can block clarity.ms with any standard tracker-blocking extension (uBlock Origin, Privacy Badger, Ghostery, Brave's built-in shields, etc.) or by enabling your browser's third-party-script blocker. You can also email us at ken.lannon@orgdrift.com to request that we exclude your account from session replay.
The services listed above are the complete set. We do not use Segment, Amplitude, Mixpanel, Heap, FullStory, HubSpot, or Intercom. Microsoft Clarity is the only session-replay tool in use and it is scoped to marketing pages. If you find an outbound request from an OrgDrift page to an origin not named on this list, that is a bug and we want to hear about it.
Adding one, and telling you first. Before any new service starts receiving data from your use of OrgDrift, we will give 30 days' notice to everyone who has given us an email address, so you have time to object or leave. Services that receive nothing of yours — our own source control, our own calendar — are added to this list when they start, without the waiting period.
We retain no scan data, because we never receive any. It is worth being exact about where it does live, though, because “we don't have it” is not the same as “it is gone.” Your files are parsed in your browser, and your results are kept in that browser's own storage — sessionStorage, localStorage and IndexedDB — so a scan is still there when you come back to the tab. That is storage on your computer, under your control, and it is not sent anywhere.
Clearing it is one click. Open the account menu and choose Delete my data. That erases every stored scan, result and derived record this browser holds, and clears the usage counter on our side. Clearing your browser data for this site does the same thing. Original values for any column we redact — Social Security numbers, dates of birth, bank details — are never written to storage at all; they are held in memory and are gone when you close the tab.
Email addresses collected through form submissions are retained until you request deletion. To request deletion of anything we hold about you — your email, your account, your billing record — email ken.lannon@orgdrift.com. We will respond within 5 business days and confirm deletion within 30 days. Payment records are the one exception: those are kept as long as tax and accounting rules require.
If we make material changes — particularly anything that affects how file data is handled — we will update the “Last updated” date at the top of this page and, where appropriate, notify users who have submitted their email. We will not retroactively apply weaker protections to data already collected.
Questions about this policy or your data? Reach us at ken.lannon@orgdrift.com or write to: