Skip to content

Repository files navigation

maskctl

A local anonymization gate for AI uploads — one static binary. Scan text, files, screenshots and clipboard content for secrets and personal data before they leave your machine — then pseudonymize consistently, and translate AI answers back afterwards. No Docker, no Python, no network.

🇩🇪 Deutsche Version: README.de.md

maskctl demo: masking a log, blocking a secret, restoring the answer

you ──► maskctl ──► [gitleaks ruleset: secrets? ⛔ hard stop]
                    [PII detection DE+EN: PERSON_1, IP_3, HOST_2 …]
                            │
                            ▼
                     safe to paste into ChatGPT / Claude / Copilot
                            │
        AI answer ──► maskctl --restore ──► original names back (local only)

Why

Every prompt you paste into a cloud AI is an upload. Logs, configs and screenshots are full of hostnames, IPs, customer names, tokens. maskctl is the gate in between:

  • Secrets block hard. The gitleaks ruleset runs in-process; if it finds credentials, maskctl exits non-zero and refuses to continue. Secrets are never "anonymized away" — they must not be uploaded at all.
  • PII gets pseudonymized, not destroyed. Built-in recognizers (email, IPs, phone numbers, credit cards with Luhn check, IBANs with checksum validation, crypto wallets, URLs, person names) plus your own regex patterns replace entities with stable placeholders (PERSON_1, IP_3). The same input always maps to the same placeholder, so the AI's answer stays coherent.
  • Round-trip. maskctl --restore maps placeholders in the AI's answer back to the originals — using a local mapping table (0600, never uploaded).
  • 100 % local, zero dependencies. One static binary. Nothing ever calls out — there is not even an HTTP client in the pipeline.

Quick start

$ git clone https://github.com/muhittink/maskctl && cd maskctl
$ ./install.sh              # builds with Go, or uses a downloaded release binary
$ maskctl --selftest

Optional tools (features degrade gracefully without them): wl-clipboard for --clip, gnome-screenshot or grim+slurp for --shot, tesseract-ocr + tesseract-ocr-deu for image redaction.

Usage

$ maskctl dump.log                 # file  → dump.log.masked + stdout
$ cat error.log | maskctl          # stdin → stdout (pipe-friendly)
$ maskctl --clip                   # anonymize clipboard in place (text or image)
$ maskctl --shot                   # area screenshot → redacted PNG
$ maskctl diagram.png              # image → diagram.redacted.png
$ maskctl --restore < answer.txt   # translate AI answer back (local only)
$ maskctl --diff dump.log          # show before/after diff on stderr

Exit codes: 0 ok · 1 secrets found, upload forbidden · 2 error.

Example

$ echo "Node fw-01 (192.0.2.17) down, contact jane.doe@corp.example" | maskctl
Node HOST_1 (IP_1) down, contact EMAIL_1

maskctl: 3 replacements (mapping: ~/.local/state/maskctl/mapping.json)

Configuration

Everything lives in ~/.config/maskctl/. The interesting file is patterns.yaml (created from config/patterns.example.yaml), re-read on every run:

score_threshold: 0.4          # ignore low-confidence matches
entities:                     # which built-in entities to replace
  - PERSON
  - EMAIL_ADDRESS
  - IP_ADDRESS
custom:                       # your own regex recognizers
  - name: internal-domain
    entity: DOMAIN
    regex: '\b[a-zA-Z0-9][a-zA-Z0-9.-]*\.(?:internal|corp|lan|local)\b'
    score: 0.9
  - name: de-license-plate    # fuzzy pattern, guarded by context words:
    entity: LICENSE_PLATE     # base score is below the threshold, a nearby
    regex: '\b[A-ZÄÖÜ]{1,3}-[A-Z]{1,2} ?\d{1,4}\b'
    score: 0.2                # trigger word boosts it (+0.35)
    context: [kennzeichen]
allowlist:                    # matches that are NEVER masked (public IPs,
  - 8.8.8.8                   # product names mistaken for persons);
  - "re:acme[ -]?\\S*"        # "re:" entries match as regex (fullmatch)
prefixes:                     # placeholder prefix per entity type
  IP_ADDRESS: IP
  DOMAIN: DOMAIN

Custom patterns support lookbehind/lookahead, (?m), (?-i:…) and are compiled case-insensitively (like before). They run with a 2-second match timeout, so a pathological regex fails loudly instead of hanging.

The example config ships recognizers for infrastructure naming (hostnames, VLAN IDs, environment names, internal domains) and a battle-tested generic Linux/IT ruleset — adapt them to your environment.

Custom secret rules: copy config/gitleaks.toml.example to ~/.config/maskctl/gitleaks.toml to add company-specific token formats to the secret gate. Keep the [extend] useDefault = true block — without it your file replaces the entire default ruleset. extend.url is rejected: maskctl never talks to the network.

How it works

Stage Component Guarantee
1. Secret scan gitleaks ruleset, in-process hard stop on any credential
2. PII detection built-in Go recognizers + your patterns.yaml merged spans — no partial redactions
3. Pseudonymization local mapping table stable, consistent placeholders across sessions
4. Image redaction tesseract OCR + same detection + black-box fill screenshots and images; secrets in images get blacked out too
5. Restore local mapping table AI answers translated back, offline

The mapping table (~/.local/state/maskctl/mapping.json, mode 0600) is the only place linking placeholders to originals. It never leaves your machine — treat it like a key.

Person names: honest limitations

PERSON detection uses embedded name dictionaries and heuristics ("Herr Müller", "Dr. Jane Doe", known first name + surname) instead of an NER model. That keeps maskctl a single dependency-free binary, but recall is lower than spaCy's: an uncommon first name without a title will slip through. Compensate with custom: patterns for your naming conventions, and allowlist: entries for false positives. The secret gate and all regex-based detection are unaffected by this trade-off.

Security posture

  • One static binary; the pipeline contains no HTTP client, no containers, no interpreters. gitleaks extend.url remote configs are rejected.
  • Overlapping detection spans are merged before replacement, so no fragment of a detected entity survives.
  • User regex patterns run with a hard match timeout (ReDoS guard).
  • --shot/--clip delete the unredacted temporary image after redaction.

Found a vulnerability? See SECURITY.md.

Migrating from the container-based version (≤ 0.1.0)

Your patterns.yaml, gitleaks.toml and mapping.json keep working unchanged — placeholders stay stable. The docker artifacts in ~/.config/maskctl/ are obsolete:

$ docker compose -f ~/.config/maskctl/docker-compose.yml down --rmi all
$ rm ~/.config/maskctl/{docker-compose.yml,Dockerfile.analyzer,analyzer-conf.yaml,nlp-conf.yaml,recognizers-conf.yaml}

Roadmap

  • Pre-commit and CI integration (gate merge requests, not just uploads)
  • Team features: shared policy config, audit log

License

Apache-2.0 © Muhittin Kesikli

About

Local anonymization gate for AI uploads — blocks secrets (gitleaks), pseudonymizes PII (Presidio DE+EN), restores AI answers locally

Topics

Resources

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages