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: May 9, 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.
The only data that ever leaves your machine is data you explicitly choose to submit — your email address for scan delivery or a contact request.
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.
Email addresses collected through form submissions are retained until you request deletion. Scan data is never retained — it exists only in your browser session memory.
To request deletion of any data we hold about you, email ken.lannon@orgdrift.com. We will respond within 5 business days and confirm deletion within 30 days.
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: