
SecretSweeper is a ⚡ fast, in-memory secret-sanitizing Python module written in Zig, designed for 🚀 speed.
💡 Just want to remove all secret variables from the terraform plan output or any large file? SecretSweeper is here to help! In a shared Terraform workspace, anyone who can edit a module can add
output "x" { value = var.db_password }– and suddenly everyone who can run plan can read a secret they were never granted access to. SecretSweeper wraps your plan/apply output stream so known secrets get redacted no matter how they end up in it, even from output blocks you didn't write.
SecretSweeper is a Python library that can mask or remove known secrets – API keys,
tokens, credentials – from byte literals, files, or any file-like objects (io.BinaryIO).
- Written in Zig with no third-party dependencies. The core is a plain C-ABI shared library driven through the standard library
ctypesmodule, so a single binary works across Python versions. - Can wrap a file descriptor to read and sanitize data directly from the stream.
- Works well with multi-line secrets.
pip install secretsweeper ✨ To mask secrets from the bytes literal:
importsecretsweeperprint(secretsweeper.mask(b"Hello, Secret Sweeper!", (b'Secret', b'Sweeper')))
# b'Hello, ****** *******!' Secrets may be completely removed by providing a third argument, limit=0, which specifies the maximum number of masking characters:
importsecretsweeperprint(secretsweeper.mask(b"Moby Dick!", [b" Dick"], limit=0))
# b'Moby!' To effectively mask all secrets in a large text:
importurllib.requestimportsecretsweeperurl="https://raw.githubusercontent.com/annotation/mobydick/main/txt/plain.txt"withurllib.request.urlopen(url) assrc, open("sanitized.txt", "wb") asdest:
stream=secretsweeper.StreamWrapper(
src, (b"Dick", b"savage", b"cannibal", b"harpooner")
)
forlineinstream:
dest.write(line)A more realistic scenario: any multi-tenant Terraform/OpenTofu setup, where someone with plan access shouldn't see secrets they weren't granted:
importjson, subprocess, secretsweeper# Plan as the trusted process. OpenTofu does NOT redact sensitive# values in JSON output, unlike its human-readable plan text.subprocess.run(["tofu", "plan", "-out=tfplan"], check=True)
plan=json.loads(subprocess.run(
["tofu", "show", "-json", "tfplan"], capture_output=True, check=True
).stdout)
# Collect every value OpenTofu marked sensitive - variables,# resource attributes, outputs - however it got there.known_secrets= {
str(v["value"]).encode()
forvinplan.get("variables", {}).values() ifv.get("sensitive")
}
# Only now render the plan a human will see - wrapped, so a leak# via output blocks (e.g. a stray nonsensitive() call) still gets caught.proc=subprocess.Popen(["tofu", "show", "tfplan"], stdout=subprocess.PIPE)
forlineinsecretsweeper.StreamWrapper(proc.stdout, tuple(known_secrets)):
print(line)The example above only walks top-level variables. Sensitive values nested inside maps, lists, or objects need a recursive walk of after_sensitive, since it mirrors the shape of after:
defcollect_sensitive(value, marker):
"""Recursively collect leaf values OpenTofu marked sensitive. `marker` mirrors the shape of `value` (dict/list of bools) per the plan JSON format's after_sensitive/before_sensitive convention. """found=set()
ifisinstance(marker, dict) andisinstance(value, dict):
forkey, sub_markerinmarker.items():
ifkeyinvalue:
found|=collect_sensitive(value[key], sub_marker)
elifisinstance(marker, list) andisinstance(value, list):
fori, sub_markerinenumerate(marker):
ifi<len(value):
found|=collect_sensitive(value[i], sub_marker)
elifmarkerisTrue:
found.add(str(value).encode())
returnfoundknown_secrets=set()
forvarinplan.get("variables", {}).values():
ifvar.get("sensitive"):
known_secrets.add(str(var["value"]).encode())
forchangeinplan.get("resource_changes", []):
after=change["change"].get("after") or {}
after_sensitive=change["change"].get("after_sensitive") or {}
known_secrets|=collect_sensitive(after, after_sensitive)
foroutinplan.get("output_changes", {}).values():
known_secrets|=collect_sensitive(out.get("after"), out.get("after_sensitive")) More examples are in tests.
SecretSweeper's Zig core is within a few percent of the fastest Rust-backed Aho-Corasick implementation available for Python, and multiple times faster than stdlib re or other pure-Python/C-extension alternatives. See benchmarks/RESULTS.md for the full, reproducible comparison (methodology, corpus, and machine specs included).
🌱 Contributions are always welcome – whether it’s a bug report, a small fix, or a big idea. If something here sparks your curiosity, jump in and help shape it. Open an issue or a pull request – even small contributions make a difference.
🪪 This is free software: you can redistribute it and/or modify it under the terms of the MIT License. A copy of this license is provided in LICENSE.