> For the complete documentation index, see [llms.txt](https://docs.luciq.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.luciq.ai/ios/setup-luciq-for-ios/luciq-pii-masking-linter.md).

# Luciq PII Masking Linter

A release gate that statically catches unmasked PII — card numbers, SSNs, emails, passwords, and more — in your app that integrated with Luciq-SDK before it ships, across the surfaces the\
SDK protects: Screenshots, Session Replay, and Network logs.

On iOS the linter is a **command-line tool** written in Python (stdlib only, zero third-party dependencies). It scans your **Swift** (`.swift`) source and can run:

* **Inline in Xcode** as warnings/errors via a Run Script build phase.
* **In CI** as a gate that fails the job on a masking gap.
* **From your terminal** for an ad-hoc check.

***

### Requirements

<table><thead><tr><th width="169.015625">Requirement</th><th>Detail</th></tr></thead><tbody><tr><td>Python</td><td><strong>3.8+</strong></td></tr><tr><td>Dependencies</td><td><strong>None</strong> (standard library only).</td></tr><tr><td>Luciq SDK</td><td><strong>≥ 14.2.0</strong> recommended — network auto-masking is on by default from this version. Older versions are flagged.</td></tr></tbody></table>

***

### Install

Use **pipx** so the command lands in `~/.local/bin` — a stable location the Xcode build can find:

```bash
pipx install luciq-masking-linter
```

This exposes the `luciq-masking-linter` command. Plain `pip install luciq-masking-linter` also works (useful in CI).

#### Inline in Xcode

To see findings **inline in Xcode**, add a **Run Script build phase**. Xcode build phases run with a minimal `PATH`, so prepend `~/.local/bin` before calling the command — otherwise the build reports the tool as "not installed" even when it is:

```bash
export PATH="$HOME/.local/bin:$PATH"
luciq-masking-linter "$SRCROOT" --format xcode --mode warn
```

With `--format xcode`, findings appear as native Xcode warnings (or errors) on the exact source line.

***

### Usage

```bash
luciq-masking-linter [path] [--mode …] [--compliance …] [--format …] [--exclude …]
```

`path` is the project root to scan (defaults to the current directory). A typical run:

```bash
luciq-masking-linter . --mode enforce --compliance gdpr --format sarif
```

#### Flags

<table><thead><tr><th width="161.046875">Flag</th><th width="173.94921875">Default</th><th>What it does</th></tr></thead><tbody><tr><td><code>--mode</code></td><td><code>warn</code></td><td><code>warn</code> reports and exits <code>0</code>; <code>enforce</code> exits <code>1</code> on a blocking gap (the gate).</td></tr><tr><td><code>--compliance</code></td><td>from <code>luciq.yml</code></td><td><code>none</code> / <code>soc2</code> / <code>pci</code> / <code>gdpr</code> / <code>hipaa</code> — overrides the file and the <code>LUCIQ_COMPLIANCE</code> env var fallback.</td></tr><tr><td><code>--format</code></td><td><code>text</code></td><td><code>text</code>, <code>xcode</code> (inline in Xcode), <code>github</code> (PR annotations), <code>sarif</code> (Code Scanning).</td></tr><tr><td><code>--exclude</code></td><td>—</td><td>A path glob or directory name to skip; repeatable. Merged with <code>exclude:</code> in <code>luciq.yml</code>.</td></tr></tbody></table>

Compliance resolution order: `--compliance` flag → `LUCIQ_COMPLIANCE` env var → `luciq.yml` → default `none`.

#### Output formats

<table><thead><tr><th width="175.828125">Format</th><th>Use for</th><th>Shape</th></tr></thead><tbody><tr><td><code>text</code> (default)</td><td>Local terminal runs</td><td>A human-readable report with a summary and verdict.</td></tr><tr><td><code>xcode</code></td><td>Xcode Run Script phase</td><td>`file:line:col: warning</td></tr><tr><td><code>github</code></td><td>GitHub Actions PR annotations</td><td><code>::warning file=…,line=…::[check] message</code> workflow commands.</td></tr><tr><td><code>sarif</code></td><td>GitHub Code Scanning / security dashboards</td><td>SARIF 2.1.0 JSON; each check id becomes a SARIF rule id.</td></tr></tbody></table>

In IDE/annotation formats a finding shows as an **error** only when it would actually block (a `fail` under `--mode enforce`); otherwise it is a **warning**.

A `text` run looks like:

```
Luciq PII Masking Linter
  platforms : ios
  compliance: gdpr    mode: enforce

  [FAIL] unmasked-field PaymentView.swift:24 — card field is not masked …
  [warn] unmasked-field ProfileView.swift:31 — phone field is not masked …

Summary: 1 fail, 1 warn
Verdict: BLOCK release
```

***

### What it checks

#### Project posture

<table><thead><tr><th width="203.140625">Check id</th><th width="144.61328125">Layer</th><th>Fires when</th></tr></thead><tbody><tr><td><code>auto-mask-config</code></td><td>Visual</td><td>No auto-mask configured (<code>setAutoMaskScreenshots</code> / <code>autoMaskScreenshotOptions</code> absent), or set to <code>MASK_NOTHING</code> / <code>maskNothing</code>, or — under HIPAA — <code>MEDIA</code> is missing from the configured types.</td></tr><tr><td><code>network-disabled</code></td><td>Network</td><td>Network auto-masking is explicitly turned off via <code>NetworkLogger.autoMaskingEnabled = false</code> (or <code>IBGNetworkLogger.autoMaskingEnabled = false</code>).</td></tr><tr><td><code>network-sdk-version</code></td><td>Network</td><td>The detected Luciq SDK version is <strong>&#x3C; 14.2.0</strong> (hard fail); or it could not be determined (warning).</td></tr><tr><td><code>consent-gate</code></td><td>Defense</td><td>Under GDPR/HIPAA: <code>SessionReplay.enabled = true</code> is not guarded by a consent check.</td></tr></tbody></table>

#### Per-field coverage (`unmasked-field`)

For each candidate field the linter asks, in order:

1. **Is it a PII candidate?** A text input or display label whose name matches a built-in or custom keyword family. Candidacy is **name-driven** by default — a `TextField` / `SecureField` is flagged only when its name matches a family, never merely for being an input. (Exception: the GDPR/HIPAA *input floor*, below.)
2. **Is it masked?** Covered by auto-mask of its type, an inline private marker, or a reference in `addPrivateViews(...)` / `setPrivateView(...)`.
3. **Is it waived?** An inline `// luciq-mask-ignore` comment.
4. If none of the above → **finding**.

Candidate detection runs on the **comment-stripped** source, so a field or widget name appearing only in a comment is never flagged, and a commented-out marker or config call never counts as real, active masking.

***

### How to fix a finding (mask the field)

Mask the flagged field with any of these:

```swift
// SwiftUI — inline marker
Text(user.email).luciq_privateView()

// UIKit — inline marker
emailLabel.luciq_privateView = true

// Or register the view by reference
Luciq.addPrivateViews(emailLabel)
```

Or clear a whole class of fields in bulk by configuring auto-mask at init:

```swift
Luciq.setAutoMaskScreenshots([.textInputs, .labels])
```

A clean setup (passes the gate) looks like:

```swift
import Luciq

class VC {
    func setup() {
        SessionReplay.autoMaskScreenshotOptions = [.textInputs, .labels]
        if userHasConsented {
            SessionReplay.enabled = true        // consent-gated
        }
    }

    var body: some View {
        Text(user.email).luciqPrivate()
        SecureField("pwd", text: $pwd)          // covered by textInputs auto-mask
    }
}
```

***

### Skip a finding (waivers)

If a flagged field isn't really PII, skip it with a comment:

* `// luciq-mask-ignore` on the field's line (or the line just above it) — waives that one field.
* `// luciq-mask-ignore-file` anywhere in a file — waives every candidate in that file.
* Add a reason for the audit trail: `// luciq-mask-ignore: masked upstream`. The reason is surfaced in the finding.

The legacy aliases `// luciq-ignore` and `// luciq-ignore-file` still work.

> **Card fields can't be waived under `pci`.** A card-family waiver (line- or file-level) is refused and reported as a hard failure when compliance is `pci`.

***

### Configure — `luciq.yml`

Put one optional file at your project root. It holds the only two things the linter cannot read from your code: the **compliance level** and **custom keyword families**. The CLI and the Android Lint check both read it. If [PyYAML](https://pyyaml.org/) is installed it is used; otherwise a built-in minimal YAML reader handles the subset this file needs.

```yaml
pii_masking:
  compliance: gdpr               # none | soc2 | pci | gdpr | hipaa
  keywords:                      # your app-specific PII field names
    ssn:    [fiscalCode, taxId]  # nest under a built-in family so severity applies
    member: [memberRef]          # a custom family (always warn — see below)
  exclude: [Generated]           # paths to skip
```

`compliance` may also come from `--compliance` or the `LUCIQ_COMPLIANCE` env var, both of which override the file.

#### Compliance levels — what blocks vs. warns

The level does not add new checks; it turns dials on the existing ones.

| Level           | Blocks on                                                                                |
| --------------- | ---------------------------------------------------------------------------------------- |
| `none` / `soc2` | card, SSN, credentials                                                                   |
| `pci`           | same, **and** card waivers are refused                                                   |
| `gdpr`          | + email, phone, name, address, DOB, **every** text input (input floor); consent required |
| `hipaa`         | all of the above + health; `MEDIA` auto-mask + consent required                          |

**The GDPR/HIPAA input floor.** Under `gdpr` and `hipaa`, masking *all* user input is mandatory, so the synthetic `input` family kicks in: every text input is a candidate regardless of name. PII inputs are normally cleared in bulk via `.textInputs` auto-mask.

#### Keyword families

Candidacy is name-driven against these built-in families. Each family's default severity is promoted to a hard fail by the levels in the last column.

<table><thead><tr><th width="124.07421875">Family</th><th width="190.65234375">Covers</th><th>Example name matches</th><th>Hard-fail under</th></tr></thead><tbody><tr><td><code>card</code></td><td>PAN, CVV, bank account, IBAN</td><td><code>cardNumber</code>, <code>pan</code>, <code>cvv</code>, <code>iban</code>, <code>accountNumber</code></td><td>all levels (no waiver under <strong>pci</strong>)</td></tr><tr><td><code>ssn</code></td><td>National / tax / social-security ids</td><td><code>ssn</code>, <code>nationalId</code>, <code>taxId</code></td><td>all levels</td></tr><tr><td><code>credentials</code></td><td>Passwords, PINs, OTPs, secrets, tokens</td><td><code>password</code>, <code>pin</code>, <code>otp</code>, <code>apiKey</code>, <code>accessToken</code></td><td>all levels</td></tr><tr><td><code>health</code></td><td>PHI — diagnosis, MRN, medication</td><td><code>diagnosis</code>, <code>mrn</code>, <code>medication</code>, <code>patientId</code></td><td><strong>hipaa</strong></td></tr><tr><td><code>dob</code></td><td>Date of birth</td><td><code>dob</code>, <code>birthDate</code>, <code>dateOfBirth</code></td><td>gdpr, hipaa</td></tr><tr><td><code>email</code></td><td>Email addresses</td><td><code>email</code>, <code>userEmail</code></td><td>gdpr, hipaa</td></tr><tr><td><code>phone</code></td><td>Phone / mobile numbers</td><td><code>phone</code>, <code>msisdn</code>, <code>mobileNumber</code></td><td>gdpr, hipaa</td></tr><tr><td><code>address</code></td><td>Postal / home address</td><td><code>homeAddress</code>, <code>street</code>, <code>postalCode</code></td><td>gdpr, hipaa</td></tr><tr><td><code>name</code></td><td>Person names</td><td><code>firstName</code>, <code>lastName</code>, <code>patientName</code></td><td>gdpr, hipaa</td></tr><tr><td><code>input</code></td><td>Synthetic: any unnamed text input</td><td>every <code>SecureField</code> / <code>TextField</code></td><td>gdpr, hipaa</td></tr></tbody></table>

**Custom keywords.** Add app-specific stems under the closest built-in family so compliance severity applies. Matching is **case- and separator-insensitive**: a stem matches camelCase, snake\_case, kebab-case, and spaced spellings alike — `fiscalCode` catches `userFiscalCode`, `fiscal_code`, `fiscal-code`, and `fiscalCodeNumber`. Keep custom keywords as **stems**, never frozen exact names.

> **Custom families are always `warn`** — even under HIPAA. Severity is promoted only for families a preset lists by name. To make an app-specific keyword **block**, nest its stem under a built-in family (e.g. `ssn`) rather than inventing a new family name.

#### Full `unmasked-field` severity matrix

`fail` blocks under `--mode enforce`; `warn` only reports; `—` means the field is not a candidate at that level.

<table><thead><tr><th>Family</th><th width="105.44140625" align="center">none</th><th width="110.21875" align="center">soc2</th><th width="108.95703125" align="center">pci</th><th width="113.7109375" align="center">gdpr</th><th align="center">hipaa</th></tr></thead><tbody><tr><td><code>card</code></td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td></tr><tr><td><code>ssn</code></td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td></tr><tr><td><code>credentials</code></td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td></tr><tr><td><code>health</code></td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center"><strong>fail</strong></td></tr><tr><td><code>dob</code></td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center"><strong>fail</strong></td><td align="center">fail</td></tr><tr><td><code>email</code></td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center"><strong>fail</strong></td><td align="center">fail</td></tr><tr><td><code>phone</code></td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center"><strong>fail</strong></td><td align="center">fail</td></tr><tr><td><code>address</code></td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center"><strong>fail</strong></td><td align="center">fail</td></tr><tr><td><code>name</code></td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center"><strong>fail</strong></td><td align="center">fail</td></tr><tr><td><code>input</code> (unnamed input)</td><td align="center">—</td><td align="center">—</td><td align="center">—</td><td align="center"><strong>fail</strong></td><td align="center">fail</td></tr><tr><td>custom family</td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td></tr></tbody></table>

#### Posture severity matrix (non-field checks)

<table><thead><tr><th width="173.2109375">Check</th><th width="97.8125" align="center">none</th><th width="91.87109375" align="center">soc2</th><th width="93.87109375" align="center">pci</th><th width="115.62109375" align="center">gdpr</th><th align="center">hipaa</th></tr></thead><tbody><tr><td><code>auto-mask-config</code> (absent / <code>MASK_NOTHING</code>)</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td></tr><tr><td><code>auto-mask-config</code> (MEDIA required)</td><td align="center">—</td><td align="center">—</td><td align="center">—</td><td align="center">—</td><td align="center"><strong>fail</strong></td></tr><tr><td>card waiver refused</td><td align="center">—</td><td align="center">—</td><td align="center"><strong>fail</strong></td><td align="center">—</td><td align="center">—</td></tr><tr><td><code>network-disabled</code></td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td></tr><tr><td><code>network-sdk-version</code> (&#x3C; 14.2.0)</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td><td align="center">fail</td></tr><tr><td><code>network-sdk-version</code> (unknown)</td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td><td align="center">warn</td></tr><tr><td><code>consent-gate</code></td><td align="center">—</td><td align="center">—</td><td align="center">—</td><td align="center"><strong>fail</strong></td><td align="center"><strong>fail</strong></td></tr></tbody></table>

***

### Exclusions

Skip paths with repeatable `--exclude <glob>` or an `exclude:` list under `pii_masking` in `luciq.yml`. A pattern matches a whole relative path (`Generated/*`) or any single path segment (`Tests` skips every directory named `Tests`). The tool also always skips `build`, `Pods`, `node_modules`, `.git`, `DerivedData`, and `Carthage`.

***

### CI

Install the tool, then run it where you want a gate. GitHub Actions:

```yaml
- uses: actions/setup-python@v5
- run: pip install luciq-masking-linter
- run: luciq-masking-linter . --mode enforce      # fails the job on a blocking gap
```

Any CI works the same: install, then `luciq-masking-linter . --mode enforce`. For inline PR annotations add `--format github`; for GitHub Code Scanning add `--format sarif` and upload the SARIF output.

#### Exit codes

* `0` — clean, or running in `--mode warn`.
* `1` — `--mode enforce` **and** at least one blocking gap.

***

{% hint style="info" icon="message-exclamation" %}
No static gate catches everything. It will not catch PII held in a generically-named field (a `value` property holding an SSN), whether a custom network key is masked server-side, or whether a region actually renders black at runtime. The last is irreducibly human. **This gate raises the floor; it does not replace the pre-production visual check.**
{% endhint %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.luciq.ai/ios/setup-luciq-for-ios/luciq-pii-masking-linter.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
