Skip to content

These pages describe PrivacyFence 7.0.2. Download it · What changed

Approvals and policy

How PrivacyFence decides whether an AI system's request runs, waits for you, or is refused — and every control you have over that: the approvals list, the approval card, the PII check, always-allow rules, the privacy filter, and notifications.

Appendices:

Every settings.yaml key mentioned here is described in full in the configuration reference.


How requests are gated

Every connector tool has one of three gates:

Gate Used for What happens
auto Low-risk lookups: listing, searching metadata, free/busy Runs straight away. Still passes through the privacy filter and is written to the audit log
review Reads that release content (an email body, a document, a page) PrivacyFence fetches the content, shows it to you on a card, and releases it only if you approve
popup Writes (drafts, messages, edits, moves, deletes) PrivacyFence shows exactly what it is about to do and does it only if you confirm

Tools reference lists the gate of every tool.

A review or popup request is decided in this order:

  1. An always-allow rule matches — the request runs without a card and the audit log records which rule matched. A read with a PII match is the exception: it always goes to a card.
  2. A same-file grace window is open — some Drive writes, see below.
  3. Otherwise it becomes a pending approval — a card on your approvals list.

The 30-second wait and approval_pending

When a request needs you, PrivacyFence holds the AI system's call open for up to 30 seconds (web.approvals.hold_window_seconds). Decide within that time and the call simply completes.

If you don't, the call returns a result with "status": "approval_pending", the approval's id, a link to the card, and an expires_at time. The AI system is told to send you that link and then call privacyfence_await_approval (or retry the same call later). Your decision is picked up when the AI system repeats the identical call — same tool, same arguments. There is no tool that fetches content by approval id: approved content only ever reaches the AI system through the original call.

Your decision applies to exactly what you saw: if any argument of the repeated call differs, or the content the card would show has changed, the repeat gets a new card (ADR 0175).

If you already have an approval waiting, a second one returns approval_pending immediately instead of waiting another 30 seconds (web.approvals.adaptive_hold, on by default), and the result points at the approvals list so you can decide them together.

Limit Default Key under web.approvals
Wait before returning approval_pending 30 seconds hold_window_seconds
An undecided approval expires after 15 minutes pending_ttl_seconds
A decision can be collected by repeating the call within 5 minutes ledger_ttl_seconds
Pending approvals per person 20 max_pending_per_principal
Pending approvals in total 50 max_pending

An approved read can be collected more than once within those 5 minutes. An approved write is used up by the first repeat of the call, so it can never run twice on one approval.

In an unattended session (a scheduled task started with privacyfence_begin_unattended_session) nobody is there to answer, so any request that would need a card is refused at once — see How it works. The flag covers only the person and the MCP session that set it. A connection that sends no MCP session id cannot be marked unattended: the call fails with an error and that connection's gated calls ask for approval as usual. Clients on the session-less MCP protocol (claude.ai is one) never send one, so a scheduled task on such a client should use a scoped token, which is deny-only without an unattended session.


The approvals list

Approvals (/approvals) is the page you get when you open PrivacyFence (it opens through the companion app on every platform). It lists every request waiting for you and updates live while it's open. In an organization deployment each person sees only their own requests, and the list is current as of when the page loaded (it does not update live).

The heading counts what is waiting, for example "4 approvals pending · 3 reads · 1 write". Each row shows:

  • the connector, the tool, and how long ago the request arrived;
  • who is asking — the AI system's name, marked "not verified" unless it is verified (see below);
  • a title that says what the request is about ("Read email: Q3 budget"), with a read or write pill;
  • three buttons, in this order: Details, Review →, Deny.

Details expands a short, metadata-only summary of the request (for example sender, file name, recipients). Review → opens the full card, which is the only place you can approve a single request. Deny refuses it outright with no note; there is no undo. To tell the AI system why, open the card with Review → and use Deny with a note…, or select the row and use Deny selected with a note… (below).

When nothing is waiting the page says "Nothing is waiting. PrivacyFence is watching." — or, if no connector has been authenticated yet, "Nothing is governed yet." with a link to Settings → Connectors.

Deciding several at once

Rows are grouped by connector and operation. Each row that can be batched has a checkbox, and there is a Select all per group and for the whole page. With rows selected:

  • Deny selected denies them all. Denying never needs a passkey.
  • Deny selected with a note… opens the card's note panel below the toolbar: the same reasons, the same 500-character note, and the line "The same note goes to every selected request." Its button names how many you are denying ("Deny 3 and send") and follows your selection. Each selected request is denied with the same reason and note, and each AI system that made one of them receives it. A request that was already decided elsewhere by then keeps the answer it had and does not get the note. Cancel or Esc closes the panel and keeps what you typed; unselecting everything closes it too. After the batch is denied, the panel starts empty again.
  • Approve selected approves them all. Its label names what you are approving, for example "Approve 12 · 9 reads, 3 writes", so a write can't hide inside a batch of reads. If step-up applies to anything in the batch, one passkey prompt covers exactly the selected set — see step-up.

Approving a batch only ever shows you the Details summary of each item, not the full card. Use Review → on any item you want to read in full first.

Two kinds of request have no checkbox and must be decided from their own card:

  • a request the PII check flagged, because it needs a second confirmation;
  • a follow-up confirmation dialog (for example "Confirm Auto-Accept Rule").

The approval card

Review → opens the card at /approvals/<id>. After you decide, the browser returns to the list with a short message saying what happened. A card that was already decided elsewhere, or has expired, says it is no longer pending and links back to the list — it can't be decided twice.

What's on a card

A read card shows, top to bottom:

  • Requested by — who is asking (below).
  • The AI system's stated reason, labelled "unverified": it is text the AI system wrote.
  • The risk section, only when the PII check matched: the categories found. The matches are also highlighted where they sit in the preview.
  • What will be provided to the AI system — one line per part of the result (for example "File metadata", "Document content") saying whether the AI system gets it in full, redacted, or not at all, as set by the privacy filter.
  • The preview — the content itself: the message, the document text, the table, an image, or the text extracted from a file (see File previews).
  • The frequency line — "First time this week" or "Seen 4 times this week", counted from the audit log, so you notice when a request has quietly become routine.

A write card shows the stated reason, the action and its target (recipients, channel, file, event, …), the content that will be written, and an Effect row: one sentence saying what the write changes and whether it can be undone ("The message is posted and cannot be unsent.").

The buttons are Deny, Allow once, and — when a rule could cover this request — one or more Always allow buttons (see Always-allow and policy rules).

Deny with a note

Next to Deny, Deny with a note… opens a panel above the buttons for telling the AI system why you said no, or what to do instead. Both parts are optional:

  • a reason, one of Stop — don't retry, Wrong target, Change the content or Try another way. Tap the chosen one again to clear it. The AI system receives it as an instruction, for example "the user says the target is wrong (recipient, file, folder, record or account); correct the target, then ask again";
  • a note of up to 500 characters, with a counter below it.

Deny and send denies the request with both; Cancel closes the panel and keeps what you typed. The note starts empty every time: nothing from the request is ever put into it, so what the AI system receives is only what you wrote. It reaches only the AI system that made this request, whether it collects the answer straight away or later through privacyfence_await_approval, and it is not kept in the audit log, which records only the reason and the note's length (ADR 0083, ADR 0084). A plain Deny still takes one tap and sends no note; the AI system is then told not to retry the same call and to ask you how to proceed.

Keyboard

  • Esc denies the request, wherever the focus is. With the note panel open, the first Esc closes the panel and keeps its text, and the next one denies (without the note).
  • In the note, Enter and Space type; Ctrl+Enter (Cmd+Enter on a Mac) is Deny and send.
  • Enter and Space never activate Allow once, so a stray keypress can't approve anything. They do activate a focused Deny or Always allow button; Always allow then opens a confirmation dialog, and in that dialog Enter can't confirm either (Esc cancels).

Who is asking

Every card and list row names the AI system that made the request. How much to trust that name depends on where it came from:

Shown as Meaning
The AI system's name and logo, with a Verified badge Organization deployments only: the OAuth client this request was made with registered only with that AI system's own redirect URI (claude.ai, ChatGPT, Gemini Enterprise), with no administrator step. See AI systems
"Says it is …" with a Not verified badge The name the AI system reported about itself, or a relabel set in agent_overrides:. PrivacyFence cannot confirm it
"Unrecognised AI system", with the claimed name in quotes if one was sent The name matches no known AI system, or none was sent

On a local install every request shows Undetected, with no name and no badge. Every AI system on the computer shares one credential, so PrivacyFence has no way to tell them apart. The three rows above apply to organization deployments only. When the name is not verified, the card's wording says "the AI system" rather than borrowing the claimed name.

The name is for your information only. It never changes what is allowed: the same request gets the same rule match and the same decision whoever claims to be asking. The audit log records the same identity and where it came from (agent_source). See ADR 0035, ADR 0036, ADR 0037 and ADR 0088.

Gmail signature on drafts

The Gmail draft tools can append your Gmail signature — the one Gmail stores for the sending address. Turn it on for every draft with Privacy Filter → Gmail → Append Gmail signature to drafts (Settings on a desktop install, Admin on an organization server; gmail.append_signature_to_drafts, off by default); each call can override it with include_signature.

The signature is fetched before the card is shown, so the card's preview shows the draft exactly as it will be saved, signature included, and a Signature row says which address's signature is appended (or that none is set). The informational PII note on write cards scans only the text the AI system wrote, not your signature, which would otherwise flag nearly every draft because of your own phone number. See ADR 0038.

Some signatures embed their images (a logo, social icons) in the message instead of linking to them, and Gmail doesn't make those images available with the signature. For a draft with formatting (body_markdown), PrivacyFence copies them from your ten most recent sent messages, where Gmail attached them when you sent them. An image it can't find there is replaced by its description. The Signature row says how many images were copied or not found. See ADR 0095.


The PII check

Before a review card is shown, PrivacyFence scans the content being released for likely personal data in Hungarian, English and German: IBANs, card numbers, national ID, tax and social-security numbers, salary information, and similar. The exact patterns, and which part of each item is scanned, are in PII detection keywords. Email addresses and phone numbers are deliberately not detected.

When something matches:

  • It overrides your rules. Even if an always-allow rule covers the request, it goes to a card.
  • The card is tinted and names the categories found, highlighting them in the preview.
  • Approving needs a second confirmation. After Allow once (or Always allow) a "Possible PII Detected" dialog asks "Are you sure you want to proceed?". Cancel (or Esc) denies the request.
  • The audit log records the category names, never the matched text.

Re-reading your own content skips the second confirmation. When a Drive file (read with drive_get_file_content, drive_sheets_get_values or drive_download_file) was last written by PrivacyFence itself and nobody has changed it since, you already saw that content on the write's card, so the read is not re-confirmed. The moment anyone else edits the file, the next read is checked normally. This is remembered only until PrivacyFence restarts.

Writes are not held by the PII check — they carry content the AI system wrote, not personal data arriving from outside. A write card may show an informational note listing categories found in the text, but it never asks for a second confirmation. The one exception is drive_upload_file: an uploaded file can be something the AI system never read, so it gets the same scan and second confirmation as a read.

Turn the check off, or turn off just IP addresses or currency amounts, on a desktop install in Settings → General → PII Detection Gate (pii_detection.enabled, pii_detection.detect_ip_addresses, pii_detection.detect_financial_figures; all on by default). In an organization deployment these settings are install-wide and edited by an administrator on Admin → Privacy Filter (the PII Detection Gate is there too), not on Settings; saving asks for a passkey. Settings is personal.


Always-allow and policy rules

An always-allow rule lets matching requests run without a card. Every rule has three parts:

  • a scope — which resources it trusts: one folder, one sender domain, "files I own", one Slack channel, …;
  • the operations it covers — shown everywhere as verbs such as read, update, send;
  • optional conditions that must also hold — "no external attendees", "no attachments", ….

A request runs without a card when any rule's scope contains the item, the rule covers the operation, and every one of the rule's conditions holds. Rules only ever add permissions; no rule takes away what another allows. With no rules, everything that is gated asks.

A fresh settings.yaml contains one rule: editing a Google contact runs without a card as long as the edit changes no email address or phone number.

Three ways to create a rule

From a card — Always allow. A card offers an Always allow button for every scope that contains the item under review, each labelled with its scope ("Always allow — this folder", "Always allow — if I own it"). A file you own that sits in a folder therefore shows two buttons; pick the one you mean. Clicking one opens a "Confirm Auto-Accept Rule" dialog that states the rule as a sentence and lists every tool it covers. Confirm writes the rule and approves this request; Cancel approves this request once and writes nothing.

A rule created from a card covers the scope's value (that folder, that label, that calendar) and only the operation you just approved — reading from a folder does not also allow writing to it. Most write cards offer no Always allow button; the ones that do propose a rule scoped to the resource the call touched. The exception is Gmail drafting, whose button writes an unconditional rule: every future draft is created without a card. Which tool offers which buttons is listed in the "Always allow" per-tool reference.

From Settings — Auto-accept. Settings → Auto-accept lists one rule per scope — for example one row per Drive folder or Gmail sender domain — as a sentence, with colour-coded verb chips, an Unblocks N tools disclosure listing exactly which tools it covers, how often it has matched ("Matched 42x, last 3 days ago", or "Never matched"), Edit verbs and ✕ Remove. Calls made with a scoped token are not counted here: they match the token's own rules. You can filter by connector, verb family or text. Add a rule takes a scope, a value where the scope needs one (comma-separated for several), and the verbs to allow; for a Drive folder, Slack channel, Jira project and the like the value must be the resource's id (not its name or title; label names and file types are the exceptions), and a value that is not shaped like an id is refused with a message saying what to enter; adding verbs to a scope that already has a rule extends that rule instead of creating a second one. Edit verbs switches verbs on or off for an existing rule: a verb you add covers all of its tools, a verb you remove stops covering its tools, and a verb the rule already had keeps exactly the tools it had. Rules that do not belong to a scope in the catalogue (for example one you wrote by hand in settings.yaml) show no Edit verbs link. When a passkey is required (step_up.require_passkey), adding, editing or removing a rule asks for it (see Security and compliance).

A rule keeps the tools it covered when you saved it. When a release adds a tool that belongs to a verb the rule already has (for example grist_import_csv under a Grist document rule that allows add), the rule's row says "These verbs also cover tools this rule doesn't allow yet:" and names them, and the call is denied until you click Update rule and confirm. That adds only those tools, keeps the rule and anything that refers to it (the audit log) unchanged, and asks for the passkey like adding a rule. A scoped token's own rules have no Update rule button: create a new token, or Duplicate it. Nothing widens a rule on its own at startup, upgrade or load; the first start after the one-rule-per-scope change merges your existing rules into one row per scope without adding any tool, and keeps the old file as settings.yaml.pre-scope-rules.bak. See ADR 0166, ADR 0172 and ADR 0174.

From the AI system — privacyfence_propose_policy_change. The AI system can propose adding, replacing or removing a rule. privacyfence_list_policy shows it the current rules and the scope catalogue, and privacyfence_check_policy predicts whether a call would run without a card. A proposal always waits for you on the same confirmation dialog; nothing is written unless you confirm, and it is refused outright in an unattended session.

Some operations can only be allowed from Settings or a proposal, never from a card. For most of them nothing on the card names a resource to scope the rule to: Apps Script projects (scope "Apps Script — project"), Gmail filters ("Gmail — anything (unconditional)") and creating a Slack group chat ("Slack — anything (unconditional)"). Grist documents (scope "Grist — document") and Miro boards (scope "Miro — board") are the exception: the card does name the document or the board, but it proposes rules only from a fixed set of scopes, and that set does not include Grist or Miro.

In an organization deployment each person manages their own rules on their own Settings page; rules are never shared between people.

Where rules are stored

All three ways write the same auto_accept: section of settings.yaml:

auto_accept:
  version: 2
  rules:
    - id: r-3f9a1c2b8e
      predicate: approved_sandbox_folder
      value: ["1CdeFghIJKLmnoPQRstuVWxyz0123456789AbCdEfGh"]
      operations: [docs.edit_content, drive.write_file, sheets.write_range]
      conditions: [[not_shared_drive, null]]
  • predicate and value are the scope (see the scope catalogue).
  • operations are internal operation keys; Settings shows them as verbs and tool names.
  • conditions is a list of [name, value] pairs (see conditions). The value is null for a condition that takes none.
  • id is derived from the predicate, value and conditions, so the same rule always has the same id — the audit log and the Auto-accept page's match counts use it.

To move your rules to another install, use Export… and Import… on Settings > Auto-accept. The file carries your always-allow rules and your step-up level only, and import only adds. See Moving a person from a desktop install.

Prefer the three surfaces above to hand-editing: they validate the rule and mint its id. A hand-edited entry that is malformed, or names an unknown predicate or condition, never matches anything. A settings.yaml that still has an auto_accept_rules: or auto_accept_grants: section is refused at startup with an error naming the section; recreate those rules on the Auto-accept page. If the file also carries migrated_to_policy_v2: true, those sections' rules are already in auto_accept:, so PrivacyFence removes the old sections and the marker, rewrites the file, and starts. See ADR 0041 and ADR 0047.

Scope catalogue

Identity scopes name specific resources; attribute scopes name a property.

Scope Kind Value Predicate(s) Matches when
Drive folder identity folder ids approved_folder, approved_sandbox_folder, parent_folder_allowlist, move_within_approved_folders the file's direct parent is one of the folders (not recursive). For an upload or new file, the destination folder. For a move or a copy, both the file's current folder and the destination must be among them
Drive file identity file ids drive.file the file is one of these
Drive file type attribute MIME types file_type_allowlist the file has one of these MIME types
Drive files I own attribute — i_am_owner, created_by_me you are an owner of the file
Drive files created this session attribute — created_this_session PrivacyFence created the file since it last started
Gmail sender identity — i_am_sender you sent the message
Gmail sender domain attribute domains trusted_sender_domain the sender's domain is one of these or a subdomain of one
Gmail recipient identity — to_is_myself, i_am_sole_recipient every to address is yours (to_is_myself); you are the message's only recipient (i_am_sole_recipient)
Gmail recipient domain attribute domains approved_recipient_domain every recipient is in one of these domains
Gmail label identity label names label_match, label_name_allowlist the message carries one of the labels, compared by name with its live labels (this lets a rule allow archiving); the label being applied or removed is one of these
Gmail query attribute one Gmail search expression gmail.query Gmail's own search, run against the message when it is read, finds it; for a thread, every message matches. A per-message operator such as in:inbox does not match a message you sent, so a thread holding your own reply is not covered by it
Gmail / Calendar — anything attribute — always_allow always (unconditional)
Gmail — anything attribute — gmail.anything always (used for Gmail filters)
Slack channel identity channel or user ids approved_channel, approved_channel_all_results, approved_recipient, send_to_myself the channel is one of these; for a search, every result is in one of them; send_to_myself takes no ids and holds only for your DM with yourself
Slack channel kind attribute — dm_with_myself, group_dm, public_channels_only the channel is your DM with yourself (a DM with anyone else, or one PrivacyFence could not confirm as yours, never matches); every group DM; every result is from a public channel
Slack mentions of me attribute — slack.mentions_of_me, slack.mentions_of_me_all_results search: every result @-mentions you; read: a thread in which at least one message does
Slack conversations I'm a member of attribute channel ids to exclude (optional) slack.my_conversations, slack.my_conversations_all_results you are a member of the conversation now (checked with Slack on each call) and it is not excluded; for a search made with member_only, every result is from such a conversation
Slack direct messages to me attribute — slack.direct_messages_to_me the channel is a one-to-one DM
Slack — anything attribute — slack.anything always (used for creating group chats)
Telegram chat identity chat ids approved_chats, approved_chats_all_results the chat is one of these; for a search, every result is in one of them
Calendar identity calendar ids personal_calendar the call's calendar is one of these
Calendar events I organize attribute — i_am_organizer you are the event's organizer
Tasks list identity task list ids approved_task_list the list is one of these; for a move, both lists are
Jira project identity project keys approved_project_keys, approved_project_keys_all_results the project (or the issue key's project) is one of these; for a search, every result is in one of them
Jira issues I report / am assigned attribute — i_am_reporter, i_am_assignee your Atlassian account is the reporter; your Atlassian account is the assignee (compared by account id, never by a name or email; ADR 0199)
Confluence space identity space keys approved_space_keys the page's space is one of these
Confluence pages I wrote attribute — i_am_author your Atlassian account is the page's author (compared by account id)
Salesforce object type attribute object names approved_object_types every requested or written object type is one of these
Salesforce report identity report ids approved_report_ids the report is one of these
Contacts label identity label names label_name_allowlist the label being applied or removed is one of these
Apps Script project identity script ids apps_script.project the script is one of these
Grist document identity document ids grist.document the document is one of these
Miro board identity board ids miro.board the board is one of these
Peek page identity page slugs peek.page the call names one of these slugs. A call without a slug (publishing a new page) never matches

Matching is case-insensitive for domains, labels, space keys, project keys and object types.

A rule whose match depends on the fetched message (a Gmail label used to archive, a Gmail query, Slack mentions, a one-to-one DM) gives unknown from privacyfence_check_policy, which decides from the call's arguments and never fetches. The gate still applies the rule when the call is made.

A Grist document rule, an approved-report rule, a Jira project search rule and a Jira Assets schema search rule also cover the export tools (grist_export_records, grist_export_sql, salesforce_export_report, jira_export_issues, assets_export_objects) for the same document, report, project or schema; the PII gate still applies to them unless the call is made with a scoped token that allows personal data (below).

Peek page. A Peek page rule names page slugs and is set only in Settings; the card offers no "Always allow" button for Peek. Read covers peek_get_comments and peek_get_stats for those pages, and add covers peek_upload_page with one of those slugs, which publishes a new version. It never covers a new page, a visibility change, a rename or a delete.

Letting personal data through

The PII check keeps personal data out of the AI system's context. The tools below send their data to a file or to a page instead, and their result holds no row, so a scoped token can let them through:

Scope The token then lets through
Grist document, Salesforce report, Jira project, Jira Assets schema grist_export_records, grist_export_sql, salesforce_export_report, jira_export_issues, assets_export_objects
Peek page peek_upload_page with the page's slug
Drive folder drive_download_file, when the file is saved or handed over through a one-time link

Personal data and scoped tokens. The permission belongs to the scoped token, not to a rule. On the New token form in Settings → Scoped tokens, tick Let exports, downloads and page uploads carry personal data. It applies only to calls made with that token, covers all of the token's rules, and is shown as "personal data allowed" in the token list. A matching call then runs without a card or a second confirmation even when the scan finds personal data, in an unattended session too, but only for the tools above and only when one of the token's rules matches. A call made without a token (an ordinary Claude session) always stops for review when personal data is found. A Drive download whose content comes back inside the tool result (an organization install returns a small file inline) keeps the check even with the box ticked. Every other tool still scans. The box exists only in Settings, behind your passkey: privacyfence_propose_policy_change cannot set it. A token proposal can ask for it, but the box starts unticked and only you can tick it. The audit log records the call as auto-accepted by the rule, with the categories found; on an organization install, creating the token is audited as "personal data allowed". Rules saved before this change that allowed personal data were converted when you upgraded: the scoped tokens that used such a rule now carry the box. See ADR 0173, ADR 0157 and ADR 0158.

Personal data in reads. A second checkbox, Let reads return personal data, lets the token's reads of Gmail messages and threads and of Slack channel history, thread replies and search, and Drive file content (gmail_get_message, gmail_get_thread, slack_get_channel_history, slack_get_thread_replies, slack_search_messages, drive_get_file_content) return content that holds personal data, when one of the token's rules covers the call. It is independent of the first box, applies only to calls made with that token, and is shown as "personal data in reads allowed" in the token list. The scan still runs: the audit log records the call as auto-accepted with the categories found. A read made without a token, or with a token that lacks the box, is still stopped (a card for an ordinary session, a denial for a token). See ADR 0219 and ADR 0254.

privacyfence_check_policy reports per call whether the personal-data scan applies to it and whether the caller's token lifts it (pii_gate, pii_gate_detail); privacyfence-sync preflight uses this to warn per step.

Conditions

Conditions narrow a rule; they never widen it. They go in the rule's conditions: list. The card and Settings offer only two of them, as calendar read scopes of their own — "no external attendees" and "non-private events"; add any other condition by editing settings.yaml.

Condition Value Holds when
older_than_days days the message is at least that many days old
within_days days the event starts within that many days from now
past_only — the event has already ended
no_attachments — the Gmail message, Slack message or Telegram message carries no attachment, file or media
no_external_attendees — every attendee's address contains your own domain
no_conferencing_link — the event has no meeting link
not_private — the event's visibility is not private
not_shared_drive — the file is not in a shared drive (it lives in someone's My Drive, whether or not it is shared with others)
no_contact_info_change — a contact edit changes no email address or phone number
in_existing_thread — the Slack message is a reply in an existing thread

no_contact_info_change and in_existing_thread are decided from the call's arguments; every other condition needs the fetched item, so privacyfence_check_policy reports a rule that uses one as "unknown" rather than guessing.

Verbs

Settings and the confirmation dialog describe operations with nine verbs in four families, colour-coded so a risky verb stands out:

Family Verbs
read read, download, search
write add, update, restructure, move
send send
destructive delete

Add creates something new (a file, an event, a task, a draft, a Grist row). Update changes something that exists, including formatting, comments, labels, completing a task, transitioning a Jira issue and changing a visibility. Restructure changes the shape of a container (Grist tables and columns, Gmail filters). Move relocates a Drive file, a task or a Gmail message (archiving), or copies a Drive file, and needs both the source and the destination folder or list in the rule where there is a destination. Older verb names (create, draft, format, comment, label, complete, transition, copy, archive, configure, share) are still accepted wherever a verb is typed and are mapped to these nine. See ADR 0171.

Same-file grace window

Some Drive writes tend to come in bursts against one file — filling a sheet range by range, editing a document paragraph by paragraph. For these six tools, clicking Allow once also allows further calls of the same tool on the same file for 5 minutes, without a card:

drive_sheets_write_range, drive_sheets_format_range, drive_sheets_insert_dimensions, drive_docs_edit_content, drive_docs_format_content, drive_add_comment.

The card says so above its buttons ("Approving this also allows further calls like this to the same file for a few minutes without asking again."). The window is not a rule: it is never written to settings.yaml and ends when PrivacyFence restarts. Deleting rows or columns, adding or renaming a sheet, replacing a whole file or document, uploading, copying and moving never open one.


Privacy filter

The privacy filter decides, per category of data, what may leave PrivacyFence at all. It runs before the card is built, so what it removes never reaches the card, the AI system or the audit log. It is a floor under your review, not a replacement for it: filtered content still needs your approval where the gate asks for it.

Each category is set to one of:

Policy Text becomes A list becomes
allow unchanged unchanged
redact [REDACTED BY PRIVACY FILTER — N characters withheld] (only the length is revealed) empty
block [BLOCKED BY PRIVACY FILTER] empty

On a desktop install, set them in Settings → Privacy Filter, which has one page per group, or in settings.yaml. On an organization server an administrator sets them on Admin → Privacy Filter. Changes apply immediately. Each group also has a default_policy for any category not listed.

Group (settings.yaml key) Settings page Categories — value in a fresh settings.yaml
privacy Gmail body allow · metadata allow · attachments block · thread_history allow
drive_privacy Drive & Sheets file_content · file_metadata · file_list · folder_structure — all allow
slack_privacy Slack message_content · user_identity · channel_list · thread_content · dm_list · group_chat_list — all allow
contacts_privacy Contacts notes block
tasks_privacy Tasks notes block
confluence_privacy Confluence search_excerpt block · attachments block

In a fresh settings.yaml every group's default_policy is block. If a group is missing from settings.yaml altogether, its default is allow on a local install and block in an organization deployment. A group that is present but malformed — an unknown policy value, a non-mapping categories — stops PrivacyFence from starting rather than falling back to allow.

The filter applies only to tools that return content from the service; writes are not filtered. Other connectors have no categories. Privacy Filter → Calendar (on Settings for a desktop install, on Admin for an organization server) has one switch instead: whether calendar_get_free_busy may show event titles and status (calendar.free_busy_full_event_details, on by default) or only busy/free blocks.

If file_metadata and file_list are set differently, PrivacyFence logs a warning at startup: a file's name and owners reach the AI system through whichever of the two tools is still allowed.

In an organization deployment the privacy filter and the PII check are install-wide: an administrator edits the install-wide privacy policy on the Admin → Privacy Filter page, and Settings does not show it. Settings is personal. A desktop install keeps its own Privacy Filter on Settings. See Organization deployment.


File previews

For attachments and files — gmail_download_attachment, confluence_download_attachment, drive_download_file, drive_upload_file — the card previews the file's content so you can see what you are releasing or uploading. The same extracted text is what the PII check scans.

Type Preview
Images shown as an image (no text is read from it)
Plain text, CSV, JSON and other text/* the text
HTML converted to Markdown
PDF the text of each page, as plain text; scanned PDFs have little or none
DOCX, PPTX text with headings, emphasis, lists and tables kept as Markdown
XLSX each sheet as a table, first 200 rows and 20 columns
ZIP a list of the first 200 files and their sizes; nothing inside is opened
Anything else no preview, only the file's name, type and size

Limits:

Limit Value
Extracted text shown and scanned first 20,000 characters
Gmail and Confluence attachments fetched for a preview up to 5 MB; larger ones show metadata only and are not scanned
Drive files fetched in full for a preview up to 5 MB; larger ones are not scanned
Local file read for an upload preview up to 5 MB
A single part inside a DOCX or PPTX 10 MiB decompressed; a larger part is skipped
drive_upload_file from a local path 50 MB
Attachments on one Gmail draft 18 MB in total

Reading a document with drive_get_file_content is different: it fetches at most the first 100 KB (102,400 bytes) of the file, and that text is what the AI system receives. The card shows, and the PII check scans, all of that text. A PDF that fits within 100 KB is rendered on the card as a PDF when the file's content is allowed by the privacy filter. A sheet read (drive_sheets_get_values) shows and scans the first 50 rows.

Macros and embedded scripts are never run, and a file PrivacyFence can't parse simply gets no preview.


Notifications

While the Approvals page is open in a browser tab:

  • the tab title shows the number waiting, for example "(3)", and screen readers announce changes — no permission needed;
  • if you allow notifications, a desktop notification appears when a new request arrives and the tab is not focused (at most one every 5 seconds; several at once become one "N approvals pending" notification). They come from your browser; nothing leaves your machine.

PrivacyFence offers to turn notifications on right after you decide a request, never on page load. You can also turn them on in Settings → General → Approval Notifications, which is also where you set how much a notification says:

Level A notification for one new request shows
minimal "1 approval pending" and nothing else
standard adds the connector and whether it is a read or a write
detailed also adds the request's title, for example "Send email to [email protected]" — which can put private content on your lock screen

web.notifications.detail is standard in a fresh settings.yaml; if the key is missing, the level is minimal. web.notifications.enabled: false turns off the title count, the announcements and the notifications together.

Organization deployments: push notifications

An organization deployment has no title count or tab notifications. It has web push instead, which reaches a phone or a browser whose PrivacyFence tab is closed:

  • right after you decide a request, the Approvals page offers to turn notifications on; on an iPhone or iPad, add PrivacyFence to your Home Screen first (the page says so);
  • a notification arrives when a new request is waiting for you, at most one every 5 seconds, and tapping it opens the Approvals page;
  • it always says only "1 approval pending" (or "N approvals pending"), the minimal level above, whatever else you could see on the page;
  • it travels through your browser's push service (Apple's, Google's, Mozilla's or Microsoft's), encrypted so that service cannot read it. Your organization can turn push off for everyone.

Details for administrators are in the organization deployment guide, and the reasoning in ADR 0081.


Step-up: passkey on approvals

When step-up is on, approving asks for your passkey (Touch ID, Face ID, Windows Hello, a security key, or your phone) — proof that a person, not a program on your computer, made the decision. It is on by default on installs from the macOS, Windows and Linux installers, with the scope writes_and_pii_reads: every approved write, and every read the PII check flagged. Denying never needs a passkey. A batch approved with Approve selected needs one prompt for the whole set.

Add a passkey at Security (/security); on a packaged install the companion app walks you through adding the first one. Any authenticator that checks your PIN, fingerprint or face works: one built into the device, a USB or NFC security key, or your phone over the browser's QR-code flow (see ADR 0055). To use the one built into Windows, set up Windows Hello first (Settings → Accounts → Sign-in options, add a PIN). If nothing built in is ready and no security key or phone is used, adding a passkey fails with a message saying the device has no built-in passkey authenticator ready. The same NotAllowedError also appears when the passkey prompt was cancelled, timed out, or opened behind the browser window — the message says which of the two it was. A device that already has a passkey for this install says so.

The scopes, require_passkey, the batch setting and exactly what a passkey does and does not prove are in Security and compliance.