https://github.com/axonops/mask
Fail-closed PII, PCI, and PHI masking for Go - 60+ built-in rules (email, PAN, SSN, IBAN, IMEI, and more), rune-aware UTF-8, zero runtime dependencies
https://github.com/axonops/mask
compliance data-masking gdpr go golang hipaa masking observability pci-dss phi pii privacy pseudonymization redaction security
Last synced: 3 days ago
JSON representation
Fail-closed PII, PCI, and PHI masking for Go - 60+ built-in rules (email, PAN, SSN, IBAN, IMEI, and more), rune-aware UTF-8, zero runtime dependencies
- Host: GitHub
- URL: https://github.com/axonops/mask
- Owner: axonops
- License: apache-2.0
- Created: 2026-04-18T03:15:24.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-14T14:21:44.000Z (30 days ago)
- Last Synced: 2026-05-14T15:08:31.837Z (30 days ago)
- Topics: compliance, data-masking, gdpr, go, golang, hipaa, masking, observability, pci-dss, phi, pii, privacy, pseudonymization, redaction, security
- Language: Go
- Homepage: https://axonops.com
- Size: 2.91 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
- Governance: governance_test.go
- Notice: NOTICE
- Cla: CLA.md
Awesome Lists containing this project
README
# mask
**String Masking for Go Services β PII, PCI, PHI, zero dependencies**
[](https://github.com/axonops/mask/actions/workflows/ci.yml)
[](https://scorecard.dev/viewer/?uri=github.com/axonops/mask)
[](https://pkg.go.dev/github.com/axonops/mask)
[](https://goreportcard.com/report/github.com/axonops/mask)
[](./LICENSE)
[](https://github.com/axonops/mask/releases/latest)
[π Quick Start](#-quick-start) | [β¨ Features](#-key-features) | [π Built-in Rules](#-built-in-rules) | [π Primitives](#-utility-primitives) | [π Docs](./docs/) | [π‘ API Reference](https://pkg.go.dev/github.com/axonops/mask)
---
**Table of contents**
- [β οΈ Status](#-status)
- [π Overview](#-overview)
- [β¨ Key Features](#-key-features)
- [β Why mask?](#-why-mask)
- [π Quick Start](#-quick-start)
- [π Built-in Rules](#-built-in-rules) β full catalogue in [`docs/rules.md`](./docs/rules.md)
- [π Utility Primitives](#-utility-primitives) β full reference in [`docs/extending.md`](./docs/extending.md)
- [π§΅ Thread Safety](#-thread-safety)
- [π‘ Fail Closed](#-fail-closed)
- [π§ Configuration](#-configuration)
- [π― Custom Rules](#-custom-rules) β regex and primitive patterns in [`docs/extending.md`](./docs/extending.md)
- [π Regulatory Context](#-regulatory-context)
- [π API Reference](#-api-reference)
- [π€ For AI Assistants](#-for-ai-assistants)
- [π€ Contributing](#-contributing)
- [π¬ Support](#-support)
- [π Security](#-security)
- [π Licence](#-licence)
---
## β
Status
`mask` is **stable** from `v1.0.0` onwards and follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html): breaking changes to the public API only in a new major version. Pin a specific tag in your `go.mod` and review the [CHANGELOG](./CHANGELOG.md) on every upgrade.
> **What's new since the last release.** `url` now passes bare hosts through verbatim instead of fail-closing ([#88](https://github.com/axonops/mask/issues/88)). `connection_string` recognises the `pass` short-form key ([#82](https://github.com/axonops/mask/issues/82)) and Cassandra-family multi-contact-point authorities ([#89](https://github.com/axonops/mask/issues/89)). `database_dsn` accepts `tcp4` / `tcp6` / `unix` / `udp` protocols ([#83](https://github.com/axonops/mask/issues/83)). `date_of_birth` recognises `YYYY/MM/DD` ([#84](https://github.com/axonops/mask/issues/84)). `postal_code` accepts spaceless UK postcodes ([#85](https://github.com/axonops/mask/issues/85)). `mac_address` accepts Cisco-dotted format ([#86](https://github.com/axonops/mask/issues/86)). `phone_number` tolerates bodies with leading separators ([#87](https://github.com/axonops/mask/issues/87)). Full notes in the [CHANGELOG](./CHANGELOG.md).
## π Overview
> **Stop leaking PII through half-baked regexes.** `mask` is the drop-in redaction library every Go service on the hot path of a log, trace, or audit stream was missing. One import. One call. The original value **never** reaches the outside world.
Hand-rolled regexes work on the inputs you tested. They leak on the ones you didn't β the email with a `+` alias, the PAN with an extra space, the phone number from a country you forgot existed, the unicode address your byte-indexed slice chopped mid-character. `mask` is built so reality can disagree with the pattern and the library **still fails safe**.
### What you get, out of the box
- π― **Format-aware by design** β preserves PAN separators, email domains, IBAN check digits, phone country codes, and geographic precision so masked fields stay useful for debugging, diffing, and support tickets.
- π‘ **Fail-closed, always** β unknown rule? `[REDACTED]`. Malformed input? Same-length mask. The original value is **never** echoed back. Not even once.
- π **Unicode-safe from day one** β rune-aware so multi-byte UTF-8 is never split mid-character. International names, CJK addresses, emoji in free-text β all handled.
- β‘ **Zero runtime dependencies** β stdlib only. No goroutines. No config files. No transitive-dependency CVEs.
- π§΅ **Thread-safe like the stdlib** β register at init, apply concurrently forever after. Same contract as `database/sql.Register`.
### See it in action
```go
mask.Apply("payment_card_pan", "4111-1111-1111-1111") // "4111-11**-****-1111"
mask.Apply("email_address", "alice@example.com") // "a****@example.com"
mask.Apply("us_ssn", "123-45-6789") // "***-**-6789"
mask.Apply("iban", "GB82WEST12345698765432") // "GB82**************5432"
mask.Apply("no_such_rule", "anything") // "[REDACTED]" β fail closed
```
> **68 built-in rules across seven categories, covering identifiers in more than a dozen jurisdictions.** PCI DSS display modes for PANs. HIPAA pseudonymisation caveats for clinical identifiers. GDPR Art. 4(5) salted hashing for user IDs. Every regulation-aware rule is documented next to the code that delivers it β no spelunking required.
---
## β¨ Key Features
| Feature | Description | Docs |
|---|---|---|
| π Rich built-in rule catalogue | 68 rules across identity, financial, health, technology, telecom, and country-specific categories | [Built-in Rules](#-built-in-rules) |
| π§© Composable primitives | `KeepFirstN`, `KeepLastN`, `KeepFirstLast`, `DeterministicHash`, `ReplaceRegex`, `ReducePrecision`, and more β every primitive is exposed both as a direct-call helper and as a factory `RuleFunc` | [Primitives](#-utility-primitives) |
| π Unicode correct | Rune-aware masking for international names, addresses, and free-text content | [Unicode correctness](#unicode-correctness) |
| π‘ Fail closed | Unknown rule returns `[REDACTED]`; malformed input returns a same-length mask; the original value is never echoed | [Fail Closed](#-fail-closed) |
| π PCI / HIPAA / GDPR aware | Jurisdiction-qualified names and regulation references in the catalogue | [Regulatory Context](#-regulatory-context) |
| β‘ Zero dependencies | stdlib only at runtime | β |
| π Hot-path safe | 1 alloc/op on every built-in rule; most rules run in 50-200 ns/op on an M2. Allocation regression guard in CI. | [Performance](./docs/performance.md) |
| π§΅ Thread-safe after init | Register at startup; apply concurrently from any number of goroutines afterwards | [Thread Safety](#-thread-safety) |
| π§ Configurable mask character | Global override via `SetMaskChar`; per-instance via `WithMaskChar` | [Configuration](#-configuration) |
| π§ͺ BDD-first testing | Every rule has a Gherkin feature file; consumer-language scenarios pin the contract | [Testing](./CONTRIBUTING.md#testing) |
| π― Custom rules in three lines | `mask.Register("my_rule", func(v string) string { ... })` β then use it like any built-in | [Custom Rules](#-custom-rules) |
## β Why mask?
> **Because `strings.Replace` fails silently, and your production logs are the wrong place to find out.**
Every Go project starts with a one-line regex and a TODO. Three outages and an audit later, it becomes a 400-line helper package nobody understands. `mask` is what that package wants to be when it grows up β fewer bugs, broader coverage, unicode-correct by default, and a fail-closed contract you can actually rely on.
| Approach | Format-aware | Unicode-correct | Built-in catalogue | Fails closed |
|---|---|---|---|---|
| Ad-hoc `strings.Replace` | No | N/A | No | No β original leaks through |
| Hand-rolled regex | Partial β author-dependent | Partial | No | No β non-match returns original |
| **`github.com/axonops/mask`** | **Yes** β 68 format-specific rules | **Yes** β rune-aware by default | **Yes** β identity, financial, health, tech, telecom, country-specific | **Yes** β unknown rule β `[REDACTED]`, malformed input β same-length mask |
## π Quick Start
### Install
```sh
go get github.com/axonops/mask
```
Requires Go 1.26 or later.
### Hello world
```go
package main
import (
"fmt"
"github.com/axonops/mask"
)
func main() {
fmt.Println(mask.Apply("email_address", "alice@example.com"))
// Output: a****@example.com
}
```
### Per-instance masker with a custom mask character
```go
m := mask.New(mask.WithMaskChar('#'))
fmt.Println(m.Apply("email_address", "alice@example.com"))
// Output: a####@example.com
```
### Registering a custom rule
```go
func init() {
_ = mask.Register("employee_id", mask.KeepFirstNFunc(9))
}
// mask.Apply("employee_id", "EMP-ACME-12345") β "EMP-ACME-*****"
```
> `Register` returns an `error` when the name fails the `^[a-z][a-z0-9_]*$` pattern, when the `RuleFunc` is nil, or when the name is already registered. All three wrap `mask.ErrInvalidRule`. Safe to discard during `init()` if you know none apply; capture it in production code to surface registration bugs early.
### Redacting an ad-hoc format with regex
For anything with a predictable textual shape that isn't in the built-in catalogue β internal IDs, tokens embedded in log lines, tenant-scoped identifiers β reach for `ReplaceRegexFunc`. It compiles the pattern once at init and returns a ready-to-register rule; Go's `regexp` is RE2-backed so there is no ReDoS risk even on adversarial input (with the RE2 feature trade-offs β no backreferences, no lookahead / lookbehind β covered in the full guide).
```go
func init() {
// Any 6-or-more-digit run embedded in free-text becomes [REDACTED].
r, err := mask.ReplaceRegexFunc(`\d{6,}`, "[REDACTED]")
if err != nil {
log.Fatalf("mask: compile free_text_digits: %v", err)
}
_ = mask.Register("free_text_digits", r)
}
// mask.Apply("free_text_digits", "Order #1234567 shipped")
// β "Order #[REDACTED] shipped"
```
Capture groups can preserve context around the secret β `(Bearer\s+)[\w-]+` with replacement `${1}****` keeps the scheme and masks the token. The full regex guide (capture groups, a cookbook of patterns, compilation caching, ReDoS safety, when NOT to use regex) lives in [`docs/extending.md#regex-based-rules`](./docs/extending.md#regex-based-rules).
### Composing primitives directly
```go
// Keep the first and last 4 runes, mask the middle β one-off, no registration.
out := mask.KeepFirstLast("SensitiveData", 4, 4, '*')
// out == "Sens*****Data"
```
### Discovering rules at runtime
```go
for _, name := range mask.Rules() {
info, _ := mask.Describe(name)
fmt.Printf("%-25s %-10s %s\n", name, info.Category, info.Description)
}
```
### Common tasks
If you are looking for the right rule for a common field, start here.
| I want to mask... | Use rule | Example |
|---|---|---|
| An email address | [`email_address`](./docs/rules.md#identity) | `alice@example.com` β `a****@example.com` |
| A credit card number | [`payment_card_pan`](./docs/rules.md#financial) | `4111-1111-1111-1111` β `4111-11**-****-1111` |
| A US Social Security Number | [`us_ssn`](./docs/rules.md#country-specific-identity) | `123-45-6789` β `***-**-6789` |
| A phone number | [`phone_number`](./docs/rules.md#telecom-and-location) | `+44 7911 123456` β `+44 **** **3456` |
| An IPv4 address | [`ipv4_address`](./docs/rules.md#technology) | `192.168.1.42` β `192.168.*.*` |
| A UUID | [`uuid`](./docs/rules.md#technology) | `550e8400-e29b-41d4-a716-446655440000` β `550e8400-****-****-****-********0000` |
| An IBAN | [`iban`](./docs/rules.md#financial) | `GB82WEST12345698765432` β `GB82**************5432` |
| A medical record number | [`medical_record_number`](./docs/rules.md#health) | `MRN-123456789` β `MRN-*****6789` |
| A JWT | [`jwt_token`](./docs/rules.md#technology) | `eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.abc` β `eyJh****.****.****.` |
| A UK postcode | [`postal_code`](./docs/rules.md#telecom-and-location) | `SW1A 2AA` β `SW1A ***` |
| A UK National Insurance Number | [`uk_nino`](./docs/rules.md#country-specific-identity) | `AB123456C` β `AB******C` |
| Any free-text secret | [`full_redact`](./docs/rules.md#utility-primitives-rules) | anything β `[REDACTED]` |
| A password field | [`password`](./docs/rules.md#technology) | any non-empty value β `********` |
| An internal / bespoke ID | see [Custom rules](#-custom-rules) | compose with `KeepFirstN`, `KeepLastN`, `KeepFirstLast` |
| A per-tenant / per-request mask | use `mask.New()` per scope | each `Masker` has its own registry and mask character β see [Multi-tenant isolation](#multi-tenant-isolation) |
For the full catalogue, see [Built-in Rules](#-built-in-rules) or call `mask.Rules()` at runtime.
## π Built-in Rules
**68 rules registered out of the box** across seven categories. Every rule is fail-closed, honours the configured mask character, and has a concrete `input β output` example in its godoc.
| Category | Examples |
|---|---|
| Utility primitives | `full_redact`, `same_length_mask`, `nullify`, `deterministic_hash` |
| Identity β global | `email_address`, `person_name`, `date_of_birth`, `passport_number` |
| Identity β country-specific | `us_ssn`, `uk_nino`, `in_aadhaar`, `br_cpf`, `mx_curp` |
| Financial | `payment_card_pan`, `iban`, `swift_bic`, `uk_sort_code` |
| Health | `medical_record_number`, `diagnosis_code`, `prescription_text` |
| Technology | `ipv4_address`, `url`, `jwt_token`, `uuid`, `password` |
| Telecom + location | `phone_number`, `imei`, `msisdn`, `postal_code`, `geo_coordinates` |
π **Full catalogue with `input β output` examples for every rule: [`docs/rules.md`](./docs/rules.md)**
Or discover them at runtime:
```go
for _, name := range mask.Rules() {
info, _ := mask.Describe(name)
fmt.Printf("%-25s %-10s %s\n", name, info.Category, info.Description)
}
```
> **π‘ Missing a rule?** If your organisation masks a data type that isn't in this catalogue β a national identifier, a financial code, a telecom format, a sector-specific identifier β **[open an issue](https://github.com/axonops/mask/issues/new?title=New%20built-in%20rule%3A%20%3Cname%3E&labels=rule-request)** and tell us what it looks like. The catalogue grew from real services; we'd rather add a rule once than have every consumer hand-roll it.
## π Utility Primitives
Every primitive is exposed twice β as a Go helper (call it directly inside a custom `RuleFunc`) and as a factory (pass it to `Register`). The three quickest:
```go
mask.KeepFirstN("Sensitive", 4, '*') // "Sens*****"
mask.KeepFirstLast("SensitiveData", 4, 4, '*') // "Sens*****Data"
_ = mask.Register("employee_id", mask.KeepFirstNFunc(9)) // factory
```
π **Full primitive table (direct-call signatures, factory signatures, registered rule names) and custom-rule patterns: [`docs/extending.md`](./docs/extending.md)**
## π§΅ Thread Safety
`Register` (both the package-level function and `Masker.Register`) MUST NOT be called concurrently with `Apply`. The contract matches `database/sql.Register`:
- Call `Register` during program initialisation, before any goroutine starts calling `Apply`.
- Once every Register call has returned, the registry is read-only and `Apply` is safe for concurrent use by any number of goroutines.
- Built-in rules are stateless pure functions. Custom `RuleFunc` implementations MUST satisfy the same contract.
Violating this contract is a data race and will be reported by the Go race detector (`go test -race`). The library does NOT `defer recover()` around custom `RuleFunc` calls β a panic in a custom rule propagates out of `Apply`, by design. Custom rules MUST NOT panic; treat a panic as a programmer error and fix it at source.
As of `v1.0.1`, the package-level registry's lazy initialisation is also safe under concurrent first-call: `Apply` and `HasRule` / `Rules` / `Describe` on a zero-value or never-touched `Masker` from many goroutines simultaneously no longer race against the built-in registrar. Previously a parallel first-caller could observe the registry between the empty-map publish and the built-in registration, falling through to `[REDACTED]` for every rule. Regression tests `TestZeroValueMasker_ParallelFirstApply` and `TestZeroValueMasker_ParallelFirstHasRule` pin the fix.
```go
// Correct β register once at init time.
func init() {
_ = mask.Register("my_rule", myMaskingFunc)
}
// Correct β isolated per-instance registry, no concurrency concerns.
m := mask.New()
_ = m.Register("tenant_rule", tenantMaskingFunc)
```
## π‘ Fail Closed
`mask.Apply` always returns a string and never an error.
- Unknown rule name β `[REDACTED]` (the value of `mask.FullRedactMarker`).
- Known rule, malformed input β a same-length mask of the configured mask character.
- Empty input β empty output (except for full-redact rules, which always return `[REDACTED]`).
This contract is uniform across every rule in the catalogue. Consumers can rely on it without per-rule knowledge.
### Unicode correctness
Every built-in rule walks the input as runes, not bytes. Multi-byte UTF-8 sequences (CJK street addresses, emoji in free-text fields, accented Latin letters stored as precomposed code points) are never split mid-character, and output is guaranteed to be valid UTF-8. This matters for dashboards, log viewers, and downstream tooling that may itself panic on invalid UTF-8. Decomposed forms (for example `e` followed by `U+0301` combining acute) are masked rune-by-rune β the library does not run full grapheme-cluster segmentation; if your data stores decomposed diacritics and you need the base letter masked together with its combining mark, normalise to NFC before masking.
## π§ Configuration
### Mask character
The default mask character is `*`. Override it globally (for the package-level registry) or per instance.
```go
// Global β mutates the package-level registry.
mask.SetMaskChar('#')
// Per instance β isolated to this Masker only.
m := mask.New(mask.WithMaskChar('#'))
```
Built-in rules read the configured character at apply time, so changes are picked up on the next call. The `password` rule honours the configured character for the 8-rune mask output.
> **Factory vs. closure for custom rules.** Factories such as `KeepFirstNFunc`, `KeepLastNFunc`, and `KeepFirstLastFunc` capture `DefaultMaskChar` at construction time and ignore later `SetMaskChar` / `WithMaskChar` overrides. If your custom rule must react to the configured character, register a closure that reads `m.MaskChar()` (or the package-level `mask.MaskChar()`) at apply time. See [`docs/extending.md`](./docs/extending.md#3-honour-per-instance-mask-character-config) for the pattern.
### Multi-tenant isolation
Each `*Masker` carries its own registry, mask character, and (where configured) hashing options. Construct one per isolation scope β per tenant, per request, per process β and the scopes cannot leak rules or configuration across one another.
```go
// A per-tenant Masker with a tenant-specific salt for deterministic hashing
// and a custom mask character. The package-level registry is untouched.
func newTenantMasker(tenantID string, salt string) *mask.Masker {
m := mask.New(mask.WithMaskChar('β’'))
_ = m.Register(
"tenant_user_id",
mask.DeterministicHashFunc(
mask.WithKeyedSalt(salt, "v1"),
),
)
return m
}
```
Per-instance `Masker`s honour the same thread-safety contract as the package-level registry β register once during initialisation for that scope, then apply concurrently. Two `Masker`s registered at the same time from different goroutines do not race with each other.
### Deterministic hashing (salt and version)
`deterministic_hash` is registered by default with no salt. For production pseudonymisation you MUST configure keyed hashing via `WithKeyedSalt(salt, version)` β the salt and version are validated atomically, so you cannot accidentally ship with one half configured:
```go
m := mask.New()
_ = m.Register(
"user_id",
mask.DeterministicHashFunc(
mask.WithKeyedSalt(os.Getenv("MASK_SALT"), "v1"),
),
)
```
Do not hard-code the salt β load it from a secret store or environment variable. Rotate the salt and bump the version together; downstream consumers can tell hashes from different generations apart by the `::` output shape. The unsalted path (`DeterministicHashFunc()` with no options) emits `:` and is only suitable for development and smoke tests. See [SECURITY.md](./SECURITY.md) for the full salt-rotation and versioning policy.
## π― Custom Rules
A custom rule is a `func(string) string` registered under a name. Regex is the default extension path and handles most ad-hoc formats; primitive factories cover the remaining "keep N runes" shapes in a one-liner.
**Regex** β reach for this first when your data has a predictable textual shape the built-in catalogue doesn't cover. `ReplaceRegexFunc` compiles the pattern once at init and returns a ready-to-register rule; Go's `regexp` is RE2-backed so there is no ReDoS risk.
```go
// Redact any 6+ digit run embedded in free-text.
r, err := mask.ReplaceRegexFunc(`\d{6,}`, "[REDACTED]")
if err != nil {
log.Fatalf("compile: %v", err)
}
_ = mask.Register("free_text_digits", r)
// mask.Apply("free_text_digits", "Order #1234567 shipped")
// β "Order #[REDACTED] shipped"
```
**Primitive factories** β for common "keep N runes" shapes:
```go
func init() {
_ = mask.Register("employee_id", mask.KeepFirstNFunc(9)) // keep first 9
_ = mask.Register("account_id", mask.KeepFirstLastFunc(3, 4)) // keep 3+4
_ = mask.Register("internal_ref", mask.KeepLastNFunc(4)) // keep last 4
}
// mask.Apply("account_id", "ACME-1234-5678") β "ACM********5678"
```
For the full regex guide (capture groups, common patterns, compilation caching, when NOT to use regex) and the other patterns (closures, per-instance mask-char, deterministic hashing, fully custom `RuleFunc`), see [`docs/extending.md`](./docs/extending.md).
## π Regulatory Context
Masking is one control in a broader compliance strategy β it is not a substitute for access control, encryption, or retention policy. The table below summarises where the library fits against common regulatory regimes. See [SECURITY.md](./SECURITY.md) for the full threat model.
| Use case | Fit | Notes |
|---|---|---|
| PCI DSS display modes for PAN | Yes | `payment_card_pan`, `payment_card_pan_first6`, `payment_card_pan_last4` match the three common display modes. `payment_card_cvv` is same-length β CVV is Sensitive Authentication Data that MUST NOT be retained post-authorisation. |
| HIPAA Safe Harbor de-identification | No | Identifier rules (including `medical_record_number`, `health_plan_beneficiary_id`) are pseudonymisation, not de-identification. Retained trailing digits combined with a date or ZIP remain re-identifiable. Register `full_redact` under the same rule name if you need Safe Harbor. |
| GDPR pseudonymisation (Art. 4(5)) | Yes, with configured salt | `deterministic_hash` with `WithKeyedSalt(salt, version)` meets the GDPR definition. Salt management, rotation, and additional access controls are the operator's responsibility. |
| GDPR anonymisation | No | No rule in this library is anonymisation β all preserved-window rules leak structure, and `deterministic_hash` is reversible given the input space. |
## π API Reference
Full API documentation: [pkg.go.dev/github.com/axonops/mask](https://pkg.go.dev/github.com/axonops/mask).
A compact summary:
| Function | Purpose |
|---|---|
| `mask.Apply(name, value)` | Apply a registered rule to a value. |
| `mask.Register(name, fn)` | Register a custom rule on the package-level registry. |
| `mask.Rules()` | Return the names of every registered rule. |
| `mask.Describe(name)` | Return the `RuleInfo` for a rule (name, category, jurisdiction, description). |
| `mask.SetMaskChar(c)` | Change the default mask character on the package-level registry. |
| `mask.New(opts...)` | Construct an isolated `Masker`. Options: `mask.WithMaskChar`. |
| `mask.HasRule(name)` | Check whether a rule is registered. |
| `mask.DescribeAll()` | Return the `RuleInfo` metadata for every registered rule. |
| `mask.MaskChar()` | Return the mask rune currently configured on the package-level registry. |
## π€ For AI Assistants
Two files at the repository root are published specifically for AI coding assistants and automated documentation crawlers:
- [`llms.txt`](./llms.txt) β a concise index (~1000 words) following the [llmstxt.org](https://llmstxt.org/) specification, with the core concepts, API surface, integration flow, and common mistakes.
- [`llms-full.txt`](./llms-full.txt) β the complete documentation corpus (`llms.txt` + README + godoc + contributing + security + requirements + generated godoc reference) concatenated in a stable order. Regenerated via `make llms-full`; CI fails if it drifts.
## π€ Contributing
Contributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for branching, commit, PR, testing and release guidance β every masking rule requires a unit test AND a BDD scenario, and coverage is held at 90 % or higher.
Before opening your first pull request:
- Sign the [Contributor License Agreement](./CLA.md) (one-time, done via a PR comment; the CLA Assistant bot walks you through it). The current list of signatories is maintained at [`CONTRIBUTORS.md`](./CONTRIBUTORS.md).
- Configure signed commits locally (GPG or SSH β see [Β§ Signing your commits](./CONTRIBUTING.md#signing-your-commits)). `main` requires signed commits and will reject unsigned merges.
- Read the [Code of Conduct](./CODE_OF_CONDUCT.md).
## π¬ Support
- **Questions, ideas, show-and-tell:** [GitHub Discussions](https://github.com/axonops/mask/discussions).
- **Bug reports / feature requests:** [open an issue](https://github.com/axonops/mask/issues/new/choose).
- **Security:** see [SECURITY.md](./SECURITY.md) β vulnerabilities are reported privately, not via the public issue tracker.
## π Security
See [SECURITY.md](./SECURITY.md) for the threat model, salt-rotation policy, and coordinated disclosure procedure. Security-sensitive issues should be reported privately per that document.
Releases are signed with SLSA/Sigstore build-provenance attestations β verify with `gh attestation verify --owner axonops`. See [SECURITY.md Β§Verifying a release](./SECURITY.md#verifying-a-release).
## π Licence
[Apache Licence 2.0](./LICENSE) β Copyright Β© 2026 AxonOps Limited.
---