> 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/android/set-up-luciq-for-android/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 is integrated with **Luciq-SDK** before it ships, across the surfaces the SDK protects: Screenshots, Session Replay, and Network logs.

On Android the linter ships as a **native Android Lint check** (a Kotlin `lint.jar`). You add it as a single `lintChecks(...)` dependency — **no Python required**. From that one dependency you get:

* **Live squiggles in Android Studio** as you type on unmasked PII fields.
* **A build/CI gate**: `./gradlew :app:lint` (and `check`) fail on a masking gap because the hard-fail issues default to `ERROR` and Lint's `abortOnError` is on by default.
* **The full engine** — compliance dialing, custom `luciq.yml` keywords, the GDPR/HIPAA input floor, and whole-project posture checks — identical to the cross-platform CLI.

***

### Requirements

<table><thead><tr><th width="252.42578125">Requirement</th><th>Detail</th></tr></thead><tbody><tr><td>Android Gradle Plugin / Lint</td><td>Built against Lint API <strong>32.0.1</strong> (AGP <strong>9.x</strong>; convention: lint version = AGP version + 23.0.0).</td></tr><tr><td>JDK</td><td><strong>17</strong> (the Lint 32.x line targets JDK 17).</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

**1.** Add JitPack to `settings.gradle.kts`:

```kotlin
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }   // ← add this
    }
}
```

**2.** Add the check to `app/build.gradle.kts`:

```kotlin
dependencies {
    lintChecks("com.github.luciqai:luciq-masking-linter:0.1.1")
}
```

***

### How it runs

The check operates in two complementary passes so findings never double-report:

1. **Per-field, live (as-you-type).** A UAST file handler scans the **in-memory editor buffer** for the file you are editing, so Android Studio shows the squiggle immediately — no save or full build required. This pass emits `unmasked-field` findings.
2. **Project posture, whole-project (batch).** Auto-mask configuration, network masking, `FLAG_SECURE`, consent gating, and SDK version depend on the *whole* project, so they run once per project during `./gradlew lint`.

Findings are reported with a `[check-id]` prefix in the message (for example `[unmasked-field] card field is not masked …`), surfaced under one of the four Lint issue ids below.

***

### The four issue ids

Use these ids in `lint { }` / `lint.xml` to tune the check.

<table><thead><tr><th width="251.3359375">Issue id</th><th width="170.92578125">Default</th><th>What it covers</th></tr></thead><tbody><tr><td><code>LuciqUnmaskedPii</code></td><td><strong>error</strong> (blocks)</td><td>A PII field (input or label) that is not masked and not waived.</td></tr><tr><td><code>LuciqMaskingConfig</code></td><td><strong>error</strong> (blocks)</td><td>Project posture: missing auto-mask or <code>MASK_NOTHING</code>, network masking disabled, <code>FLAG_SECURE</code> overridden, missing consent gate, or an old SDK.</td></tr><tr><td><code>LuciqUnmaskedPiiAdvisory</code></td><td>warning</td><td>A field finding that does not block at the current compliance level.</td></tr><tr><td><code>LuciqMaskingConfigAdvisory</code></td><td>warning</td><td>A non-blocking posture advisory.</td></tr></tbody></table>

Severity → issue mapping inside the engine:

* hard-fail field → `LuciqUnmaskedPii` (ERROR)
* soft-warn field → `LuciqUnmaskedPiiAdvisory` (WARNING)
* hard-fail posture → `LuciqMaskingConfig` (ERROR)
* soft-warn posture → `LuciqMaskingConfigAdvisory` (WARNING)

All four issues are in the `SECURITY` category, vendor "Luciq".

***

### Tuning — Lint settings

Out of the box the check **blocks**: `./gradlew :app:lint` (and CI) fail on a masking gap. You tune everything in `app/build.gradle.kts`, inside `android { lint { … } }`.

**Most common: show findings as warnings only (squiggles, but never fail the build).**

```kotlin
android {
    lint {
        warning += "LuciqUnmaskedPii"     // demote the two blocking checks…
        warning += "LuciqMaskingConfig"   // …to non-blocking warnings
    }
}
```

You still get the editor squiggles and report entries, but the build stays green.

Other knobs in the same `lint { }` block:

| Want to…                                            | Add this                               |
| --------------------------------------------------- | -------------------------------------- |
| Turn a check off completely                         | `disable += "LuciqUnmaskedPii"`        |
| Keep errors but don't fail the build (project-wide) | `abortOnError = false`                 |
| Block only *new* findings                           | `baseline = file("lint-baseline.xml")` |
| Write SARIF for GitHub Code Scanning                | `sarifReport = true`                   |

`abortOnError` is Lint's own default-on switch: an `ERROR` finding fails `lint` / `check` / CI unless you disable it. Per-issue severity (`warning +=`, `disable +=`) and `lint.xml` also work as with any Lint check.

***

### What it checks

#### Project posture (`LuciqMaskingConfig` / `…Advisory`)

<table><thead><tr><th width="209.19140625">Check id</th><th width="153.64453125">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>setAutoMaskScreenshotsTypes</code> absent), or it is set to <code>MASK_NOTHING</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>Luciq.setNetworkAutoMaskingState(Feature.State.DISABLED)</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 the version could not be determined (warning).</td></tr><tr><td><code>flag-secure</code></td><td>Defense</td><td><code>ignoreFlagSecure(true)</code> is present — it overrides <code>FLAG_SECURE</code>, so secure windows get captured.</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 (`LuciqUnmaskedPii` / `…Advisory`)

For each candidate field the engine 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` / `EditText` 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 that appears 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:

```kotlin
// Jetpack Compose — inline marker
Text(user.email, modifier = Modifier.luciqPrivate(isPrivate = true))

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

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

```kotlin
Luciq.setAutoMaskScreenshotsTypes(MaskingType.TEXT_INPUTS, MaskingType.LABELS)
```

A clean setup (passes the gate) looks like:

```kotlin
class MainActivity {
    fun onCreate() {
        Luciq.setAutoMaskScreenshotsTypes(MaskingType.TEXT_INPUTS, MaskingType.LABELS)
        if (userHasConsented) {
            SessionReplay.enabled = true        // consent-gated
        }
    }

    @Composable
    fun Profile(user: User) {
        Text(user.email, modifier = Modifier.luciqPrivate(isPrivate = true))
        val passwordField = EditText(context)  // covered by TEXT_INPUTS 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**. Both this Lint check and the CLI read it. The check looks for `luciq.yml` (or `luciq.yaml`) in the nearest ancestor directory of the module.

```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: [app/generated]       # paths to skip
```

> On Android, compliance comes **only** from `luciq.yml`. The CLI's `--compliance` flag and `LUCIQ_COMPLIANCE` env var are command-line concepts and do not apply to the Lint check.

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

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

<table><thead><tr><th width="211.90234375">Level</th><th>Blocks on</th></tr></thead><tbody><tr><td><code>none</code> / <code>soc2</code></td><td>card, SSN, credentials</td></tr><tr><td><code>pci</code></td><td>same, <strong>and</strong> card waivers are refused</td></tr><tr><td><code>gdpr</code></td><td>+ email, phone, name, address, DOB, <strong>every</strong> text input (input floor); consent required</td></tr><tr><td><code>hipaa</code></td><td>all of the above + health; <code>MEDIA</code> auto-mask + consent required</td></tr></tbody></table>

**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 `TEXT_INPUTS` 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.

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

**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; `warn` only reports; `—` means the field is not a candidate at that level.

<table><thead><tr><th>Family</th><th width="107.23046875" align="center">none</th><th width="101.296875" align="center">soc2</th><th align="center">pci</th><th 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>Check</th><th width="101.23828125" align="center">none</th><th width="98.38671875" align="center">soc2</th><th align="center">pci</th><th 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>flag-secure</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>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 an `exclude:` list under `pii_masking` in `luciq.yml`. A pattern matches a whole relative path (`app/generated/*`) or any single path segment (`Tests` skips every directory named `Tests`). The check also always skips `build`, `Pods`, `node_modules`, `.git`, `DerivedData`, and `Carthage`.

***

### CI

On Android, CI needs **no Python** — just run Lint:

```yaml
- run: ./gradlew :app:lint     # fails the job on a blocking gap (abortOnError default)
```

For GitHub Code Scanning, enable `sarifReport = true` in the `lint { }` block and upload the generated SARIF.

`./gradlew :app:lint` fails the build on a blocking gap the same way the CLI's `--mode enforce` does — unless you set `abortOnError = false`.


---

# 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/android/set-up-luciq-for-android/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.
