agentleFS
engine9-aiengine9-ai/skills/e9-pii-free/SKILL.md

e9-pii-free

Skill1 starsChanged 48 days agoApache-2.0View on GitHub

Make an engine9 account PII-free (limited-pii): no plaintext names, email, phone, or postal address in the warehouse, with SHA-256 hashes for matching instead. Covers what PII-free means, the exclude_pii setting (the one check core and the server read), turning it on for a new account, the server pii-removal flow for an existing account (plan, hash capture, uninstall, verify), hash capture alone via backfillPersonHash, and troubleshooting. Use when someone asks for a PII-free, limited-pii, hash-only, or no-plaintext account, exclude_pii, or asks why names, emails, or phones still appear on such an account.

We split this skill into the rules it gives an agent

3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Default export table lists and inventory

    line 49
  2. Very specific:

    Run again without dry_run to write.

    line 147
  3. Very specific:

    Read it with GET /data/pii/summary (204 until the first audit), MCP file pii/summary.json.gz, or Conductor /pii.

    line 180

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

PII-free accounts (limited-pii)

A PII-free account stores no plaintext name, email, phone, or postal address in its warehouse. People are matched and found by SHA-256 hashes (person_hash_email, person_hash_phone) and plugin remote ids, so the account can still dedupe, segment, attribute, and report. Use this skill to turn the feature on, remove leftover plaintext from an existing account, verify it, and troubleshoot.

Quick reference

QuestionAnswer
What makes an account PII-freeexclude_pii is true on @engine9/schemas/utilities/limited-pii. That is the one check core and the server read for installs, imports, exports, and reports
What new-account setup installsWith the setting on, installDefaultPlugins installs the stack @engine9/schemas/stacks/limited-pii (an install list: person_hash and no plaintext plugins)
What is stored insteadperson_hash_email, person_hash_phone (SHA-256 *_hash_v1, MD5 *_hash_md5), plus person_remote ids
Plugins that must not be installedperson_name, person_email, person_phone, person_address, transaction/contact_details, and stacks/standard
New account (MCP)plugin install the utility, plugin setSetting exclude_pii true, then installDefaultPlugins (or bootstrapAccount with exclude_pii: true on the server)
Existing accountSchedule built-in flow pii-removal, review the plan, confirm, verify. See Existing account
Hash search without removalTask PIIWorker.backfillPersonHash (installs person_hash and backfills)
Regular auditBuilt-in flow pii-audit (PIIWorker.buildPiiSummaryFile) writes <account root>/pii/summary.json.gz; Conductor /pii renders it. See Regular audit
Check itplugin settings, plugin upgrade dry run (pii_free: true), sql tables filter person_, PIIWorker.verifyRemoval after a removal
Look people upBy hash, or by remote_person_id. lookup by name matches nobody
Plugin pathRole
@engine9/schemas/utilities/limited-piiHolds exclude_pii. Installs no tables
@engine9/schemas/stacks/limited-piiInstall list only. Installs plugin, person, person_remote, segment, source_code, timeline, transaction/core, person_hash. Does not refuse installs by itself
@engine9/schemas/person_hashHash match keys. Part of the stack. Installable alone

Concepts

The setting

exclude_pii on @engine9/schemas/utilities/limited-pii is the only answer to "is this account PII-free?" Core and the server read it for:

  • Install refusal (plaintext plugins and stacks/standard)
  • installDefaultPlugins choosing the limited-pii stack
  • Skipping leftover plaintext plugin rows in the inbound weave
  • Default export table lists and inventory

The limited-pii stack is what new-account setup installs when the setting is on. Libraries do not detect the stack, and the stack has no exclude list. Removing plaintext from an account that already has it is a reviewed server process, not flipping the setting alone.

Different rows, not a flag in the load. When a load starts it reads the plugin table and weaves the inbound steps of whatever is installed. A PII-free account has person_hash steps and no plaintext steps (and with the setting on, leftover plaintext rows are skipped). See the inbound pipeline.

Hashing. Email is trimmed and lowercased, then SHA-256 hashed to email_hash_v1. Phone is normalized to digits (+1 for US ten-digit numbers), then hashed to phone_hash_v1. MD5 (*_hash_md5) is computed only from plaintext and is empty forever for hash-only sources. A source that already supplies email_hash_v1 / phone_hash_v1 is accepted as is. Hashes are pseudonymous match keys, not anonymous: anyone holding an email can compute its hash and test for a match.

What the setting stops. It stops new installs of plaintext plugins and removes plaintext steps from the load. It does not delete anything that already exists. Existing plaintext is hashed, then removed, by the pii-removal flow.

What is PII-free and what is not

DataPII-free account
person (id, created_at, modified_at)Kept
person_hash_email, person_hash_phoneKept (hashes)
person_id_email_hash_v1, person_id_phone_hash_v1, person_id_remote_person_idKept (hashes and vendor ids)
person_remoteKept (plugin-scoped vendor ids). Review if a vendor id is an email
timeline, transaction (core columns), source codes, models, global message tablesKept
person_name, person_email, person_phone, person_addressNot installed
transaction contact columns (given_name, family_name, street_1, street_2, city, region, postal_code, country, email, phone, employer, occupation)Not installed
PersonWorker.search given_names / last_namesMatch nobody
PersonWorker.search emails / phonesHash the query and match person_hash_*
Outbound appendName, {{given_name}}Empty. There is nothing to merge
Default exportOmits the four plaintext person tables. Includes the hash tables

Rule: Hashes are not a substitute for consent or policy review. Tell the account owner they are pseudonymous.

Workflow

New account

Two steps, in this order. Sign in first (e9-mcp).

- [ ] plugin install  @engine9/schemas/utilities/limited-pii   (returns the plugin id)
- [ ] plugin setSetting  plugin_id=<that id>  name=exclude_pii  value=true
- [ ] installDefaultPlugins (MCP task, or e9 pluginworker installDefaultPlugins -a <account_id>)
- [ ] Verify
{ "command": "install", "account_id": "<account_id>", "path": "@engine9/schemas/utilities/limited-pii" }
{ "command": "setSetting", "account_id": "<account_id>", "plugin_id": "<utility plugin id>", "name": "exclude_pii", "value": true }
e9 pluginworker installDefaultPlugins -a <account_id>

On the server, bootstrapAccount({ exclude_pii: true }) does both steps (utility + setting, then default plugins).

Rule: Set exclude_pii before installDefaultPlugins. The setting is what steers the install and blocks plaintext plugins. The stack is only the install list.

Existing account: pii-removal

Server only. Core sites that already hold plaintext are out of scope for this flow; they can start PII-free (new account) and respect the setting, but removal/uninstall/backfill live on PIIWorker.

Hashes are captured before any plaintext is dropped. MD5 exists only where plaintext existed. Hashes are pseudonymous.

- [ ] Schedule flow_id "pii-removal" (first task paused for review)
- [ ] Review the plan (names and counts only). Set owner choices on createRemovalPlan if needed:
      discard_names, drop_findings ("table" or "table.column")
- [ ] Confirm: un-pause / run removePii with the plan hash
- [ ] verifyRemoval
{ "action": "schedule", "account_id": "<account_id>", "flow_id": "pii-removal" }

createRemovalPlan writes a plan file with plan_hash. Sections include plaintext plugin rows, person name columns, hash-capture counts, legacy tables, other scan findings, raw input-store files (report only), and blockers. Approval is the plan hash passed to removePii / verifyRemoval.

removePii order: install person_hash → turn on exclude_pii → migrate legacy person_identifier to compact if needed → backfillPersonHash → gate (nothing still missing) → uninstallPlugin each plaintext plugin with drop → upgrade with drop_name_columns (and discard_names when chosen) → drop legacy and approved findings → installDefaultPlugins.

MCP plugin uninstall calls PIIWorker.uninstallPlugin for a single plugin (account admin). Options: id or path, drop, dry_run.

Rule: Do not drop a table, column, or plugin row without the owner's approval of the plan hash. Report the plan first.

Rule: Never pass discard_names unless the owner chose it. Names on person have nowhere else to go on a PII-free account.

Rule: Raw input-store files are reported only. Deleting them breaks future identity rebuilds and is a separate owner decision. Backfilling hashes from raw files is only through a separate identity rebuild.

Hash capture alone

Any server account can run backfillPersonHash without removing anything: it installs person_hash, backfills from person_email / person_phone / transaction contact columns / person_identifier hash rows, and search and segments by hash work immediately.

{
  "action": "schedule",
  "account_id": "<account_id>",
  "path": "@engine9/plugins/e9workers:PIIWorker",
  "method": "backfillPersonHash",
  "options": { "dry_run": true }
}

Run again without dry_run to write. Re-runs write nothing new. missing is the gate the removal flow uses before drops.

Verify

CheckCallExpect
Settingplugin settings { "account_id": "<account_id>" }exclude_pii value: true on the utility
Installed pluginsaccount search with ids and include_plugins: trueperson_hash and the stack present. None of the five plaintext paths
Upgrade viewplugin upgrade with dry_run: truepii_free: true, no refresh.failed for refused plaintext plugins
Tablessql tables with filter: "person_"person, person_remote, person_hash_email, person_hash_phone, person_id_*. No person_name / person_email / person_phone / person_address
person columnssql describe table: "person"id, created_at, modified_at only
transaction columnssql describe table: "transaction"None of the contact columns above
After removalPIIWorker.verifyRemoval with the plan path and hashScan clean for planned drops; hash counts at least the plan's hashable distinct people; identity lookup by a known email still finds the same person_id

Rule: pii_free: true means the setting is on. It does not prove the data is gone. Always run the table and column checks (and verifyRemoval after a removal).

Regular audit (pii-audit)

PIIWorker.buildPiiSummaryFile scans every base table with the same sampling and classifier as scanTables and writes <account root>/pii/summary.json.gz. It is read only and works on any account, PII-free or not. Schedule the built-in flow pii-audit on a recurring basis to catch PII that new plugin loads bring in.

task schedule { "account_id": "<account_id>", "flow_id": "pii-audit" }
FieldMeaning
exclude_piiThe account setting at audit time. When true, every pii column is a problem
summarytable_count, tables_by_status, tables_with_pii, columns_with_pii, columns_to_review, pii_types (columns per type)
tables_with_piiTable name to its PII column names, the same shape as scanTables
tables[].statuserror (scan failed; the audit continues), pii, review, empty (no rows sampled), clean
tables[].columns[].statuspii (counted as PII), review (a weaker non-identifier finding), insufficient_data, clean
tables[].columns[].findingsClassifier type, status, confidence, examined, matching_rows, reason codes. Never examples or values

Read it with GET /data/pii/summary (204 until the first audit), MCP file pii/summary.json.gz, or Conductor /pii. Run the audit on demand with POST /data/pii/build or the Run audit button.

Rule: The summary holds table and column names, statuses, and counts only. Do not add sample values to it.

Rules

Rule: exclude_pii is the one check. The limited-pii stack is only what new-account setup installs.

Rule: Never install @engine9/schemas/stacks/standard, person_name, person_email, person_phone, person_address, or transaction/contact_details on a PII-free account. With exclude_pii on, core refuses them.

Rule: Do not read names, emails, or phones to prove a leak. Report column names, counts, and a masked pattern, not the values.

Rule: Do not add a plugin that writes plaintext (custom fields, vendor tables) to a PII-free account without the owner's review.

Troubleshooting

SymptomCause and fix
Plaintext still lands after the stack was installedexclude_pii is off, or a plaintext plugin row still exists and the setting is off. Turn the setting on (weave skips leftovers) and remove the row/table via pii-removal or plugin uninstall
Cannot install <path>: excluded by plugin setting exclude_piiWorking as designed. Do not install it. Use hashes
exclude_pii is not in plugin settingsThe utility is not installed. Install @engine9/schemas/utilities/limited-pii
lookup by name returns nothingExpected. Names are not stored. Look up by email hash, phone hash, or remote_person_id
{{given_name}} renders emptyExpected. Nothing to merge
Default export still has person_emailexclude_pii is off, or the caller passed an explicit tables list
drop_name_columns is blockedThe account is PII-free and people have a name on person. The owner must choose discard_names
upgrade dry run shows pii_free: false after the stack was installedThe setting is false or the utility is missing. The stack alone is not PII-free
removePii stopped at the hash gatebackfillPersonHash dry run still reports missing rows. Re-run the backfill; do not drop plaintext until missing is zero
Hash search finds nobody after a manual table dropperson_hash_* was never backfilled. Run backfillPersonHash before dropping plaintext, or use pii-removal

Related documentation

  • Person hash plugin
  • Inbound pipeline and the weaver
  • Install plugins on accounts
  • Upgrades and drop_name_columns
  • Person identity
  • Exports
  • Custom fields
  • Task API / flows

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.