CLI tool to audit and remove person properties from PostHog. Built for PII data cleanup when PostHog doesn't support bulk property removal via the UI.
- Audit -- Scan all person profiles for any specified property and export CSV reports
- Cleanup -- Remove properties from person profiles via PostHog's batch API (
$unset) - PII detection -- Automatically flags distinct_ids that contain emails, phone numbers, or names
- Safe defaults --
--dry-runto preview,--testto target a single person,--group-a-onlyto skip PII-only distinct_ids - Multi-property support -- Audit or clean up multiple properties in a single run
- Rate-limit handling -- Automatic retry with exponential backoff on HTTP 429 responses
- Python 3.8+
- A PostHog account with API access
- A Personal API Key with
personandqueryread scopes - Your Project API Key (starts with
phc_)
git clone <repository-url>
cd PosthogDataCleanupTool
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
cp .env.example .envThen edit .env with your credentials (see Configuration below).
Create a .env file from the example:
POSTHOG_PERSONAL_API_KEY=phx_your_personal_api_key_here
POSTHOG_PROJECT_API_KEY=phc_your_project_api_key_here
POSTHOG_PROJECT_ID=your_project_id_here
POSTHOG_API_BASE_URL=https://us.posthog.com
| Variable | Where to find it | Notes |
|---|---|---|
POSTHOG_PERSONAL_API_KEY |
PostHog > Settings > Personal API Keys | Must have person and query read scopes. Used for reading person profiles and events. |
POSTHOG_PROJECT_API_KEY |
PostHog > Project Settings > Project API Key | Starts with phc_. Used for sending $unset events via the batch API. |
POSTHOG_PROJECT_ID |
PostHog > Project Settings | Numeric project ID shown in settings or in any PostHog URL. |
POSTHOG_API_BASE_URL |
Depends on your hosting region | https://us.posthog.com (US Cloud), https://eu.posthog.com (EU Cloud), or your self-hosted URL. |
# Audit a single property
python audit.py <property>
# Audit multiple properties
python audit.py <property1> <property2> <property3>This creates a timestamped directory under audits/ with:
all_persons.csv-- every person who has the propertypersons_no_safe_distinct_id.csv-- persons whose only distinct_ids are PII (need special handling)persons_with_pii_in_distinct_ids.csv-- persons with PII in any distinct_idsummary.json-- machine-readable summary
CSV columns: distinct_id, uuid, <property>, has_safe_id, has_pii_in_ids
# Preview what would be removed (no changes made)
python cleanup.py <property> --dry-run
# Test on a single person first
python cleanup.py <property> --test <distinct_id>
# Remove from persons with safe (non-PII) distinct_ids only
python cleanup.py <property> --group-a-only
# Remove from ALL persons who have the property
python cleanup.py <property>
# Remove multiple properties at once
python cleanup.py <property1> <property2> --dry-run- Audit first --
python audit.py <property>to understand the scope - Review the CSVs -- check
persons_no_safe_distinct_id.csvfor edge cases - Dry run --
python cleanup.py <property> --dry-runto preview - Test one person --
python cleanup.py <property> --test <some_distinct_id> - Clean up safe group --
python cleanup.py <property> --group-a-only - Clean up remaining --
python cleanup.py <property>(after handling PII-only distinct_ids) - Verify --
python audit.py <property>again to confirm removal
| Flag | Description |
|---|---|
--dry-run |
Preview affected persons without making any changes |
--test DISTINCT_ID |
Send $unset to a single person for testing |
--group-a-only |
Only process persons that have at least one safe (non-PII) distinct_id |
-
Audit queries
GET /api/projects/{id}/personswith a property filter using the Personal API Key. It paginates through all results and classifies each person's distinct_ids as safe (UUIDs, numeric IDs) or PII (emails, phone numbers, name-like strings). -
Cleanup sends
$setevents with a$unsetpayload viaPOST /batch/using the Project API Key. PostHog processes these events asynchronously to remove the specified properties from person profiles. -
The
$unsetoperation removes only the specified properties. The person profile, their events, and all other properties remain untouched. -
Persons are processed in batches of 500, with automatic retry and exponential backoff on rate limits.
- Cleanup is irreversible. Once properties are unset, there is no undo. Always run
--dry-runfirst. - PostHog processes events asynchronously. After cleanup, wait a few minutes before re-auditing to verify.
- PII-only distinct_ids need care. Persons flagged in
persons_no_safe_distinct_id.csvhave no UUID-based distinct_id -- sending events to their PII-based distinct_id may re-associate PII. Handle these separately. - This tool does not delete events. It only removes properties from person profiles. Event-level properties with PII require a separate approach (e.g., PostHog's data deletion request).
- Rate limits apply. The tool handles HTTP 429 responses automatically, but large datasets may take time.
PosthogDataCleanupTool/
├── config.py # Shared configuration -- loads .env and defines constants
├── api_client.py # PostHog API helpers, PII detection, retry logic
├── audit.py # Audit command -- scans persons and exports CSV reports
├── cleanup.py # Cleanup command -- sends $unset events to remove properties
├── requirements.txt # Python dependencies
├── .env.example # Template for environment variables
├── .gitignore # Git ignore rules
└── README.md # This file