NDA Guard
Python 3.11+ library and CLI for NDA metadata, Git protection, and public bundles.
Works with any project on Linux and Windows. The core has no Python dependencies;
Git integration requires the git executable on PATH.
Install
Install from PyPI after the first release:
python -m pip install ndaguard
Run commands from the project you want to protect, or pass --root PATH:
python -m ndaguard init vendor-private --agreement NDA-001 --owner hardware-team
python -m ndaguard validate
python -m ndaguard scan --nda-only
python -m ndaguard explain vendor-private/design.pdf
Existing documents need descriptors before publication checks can protect them.
init DIRECTORY protects that entire directory, including future descendants.
Without a directory, init protects the whole policy root. It never replaces an
existing descriptor. Global options precede commands:
ndaguard --root PATH --json scan --nda-only.
Descriptors
Place .nda.toml or .nda.json in relevant directories. Patterns use / and
are relative to the descriptor's directory. Both formats can coexist.
version = 1
[[rules]]
id = "vendor-material"
include = ["private/**", "designs/**/confidential-*.pdf"]
exclude = ["private/public-release/**"]
classification = "restricted"
agreement = "NDA-001"
owner = "hardware-team"
description = "Vendor documents"
tags = ["vendor", "hardware"]
audiences = ["approved-engineers"]
purposes = ["internal-design"]
expires = "2027-12-31"
[rules.metadata]
review_ticket = "LEGAL-123"
To protect an entire directory, its descriptor can simply contain:
{
"version": 1,
"defaults": {
"classification": "restricted",
"agreement": "NDA-001",
"owner": "hardware-team"
}
}
- The strongest classification wins:
restricted > confidential > public. Confidential and restricted content is always blocked from Git and bundles. - Parent restrictions cannot be overridden by children.
defaultsapplies to the whole directory and supplies omitted rule metadata. Use explicit rules with exclusions when some descendants should remain public. - Exclusions affect only their own rule. Expiry is advisory and never releases content automatically. Audiences and purposes describe policy, not export permissions.
*.pdfmatches directly in scope;**/*.pdfmatches any depth;private/andprivate/**cover descendants.*,?, and character classes are supported.- Rule classification defaults to
restricted, and include defaults to["**"]. Public rules can independently setdeny_gitordeny_bundletotrue. - Unknown fields, malformed descriptors, unsafe patterns, and traversal fail closed. Custom metadata must be JSON-serializable.
Unmatched files are public by default. Use --unclassified restricted for an
allowlist, then explicitly classify approved paths as public. Installed hooks
retain this option. The library accepts Catalog(root, unclassified="restricted").
Library and bundles
from ndaguard import Catalog, export_zip
from ndaguard.git import GitRepository
catalog = Catalog("/path/to/project")
info = catalog.inspect("vendor-private/design.pdf")
print(info.classification, info.agreements, info.tags, info.descriptors)
metadata = info.to_dict() # Includes applicable rules, owners, expiry, and custom metadata.
safe_inputs = catalog.filter_paths(["src/main.py", "vendor-private/design.pdf"])
inventory = catalog.scan(nda_only=True, agreement="NDA-001")
plan = catalog.bundle_plan(safe_inputs)
export_zip(catalog, "/path/outside/project/public.zip", paths=safe_inputs)
violations = GitRepository(catalog.root, catalog=catalog).check_staged()
Catalog is a snapshot; call refresh() after descriptor edits. inspect_blob
evaluates virtual/Git content with content=bytes or a trusted sha256= digest.
can_include(path, purpose="git" | "bundle") checks a single path.
python -m ndaguard --json bundle ../public.zip # plan
python -m ndaguard bundle ../public.zip --apply # export
ZIP exports exclude restricted files, descriptors, links/junctions, and generated
directories. Output must be outside the policy root; replacing output requires
--overwrite. Exports are reproducible, recheck policy and exact bytes, retain
initial restrictions, and publish atomically. Use a stable source tree; this is
not a sandbox against malicious concurrent changes. Internal plans can expose
restricted filenames and metadata.
Discovery/bundles skip .git, .hg, .svn, .venv, venv, node_modules,
__pycache__, .pytest_cache, .mypy_cache, .ruff_cache, build, and dist.
Keep policies outside those directories. Git checks still inspect all tracked
descriptors. Windows matching is case-insensitive by default; case_sensitive=
can override it.
Copies and fingerprints
python -m ndaguard fingerprint vendor-private/design.pdf
python -m ndaguard fingerprint vendor-private/design.pdf --staged
Add the resulting digest to a rule's sha256 = ["64-character-hex-digest"].
Use include = [] for fingerprint-only rules. Fingerprints match identical bytes
anywhere in the tree, regardless of descriptor scope or exclusions. Path and
fingerprint matches combine with OR semantics. Git also tracks restricted blob
identities to detect unchanged copies/renames without explicit fingerprints.
For normalized text, record working-tree and staged hashes or disable Git text
normalization. Edited/re-encoded/embedded copies need additional rules or hashes.
Git protection and purge
Run from the full root of an existing, non-bare Git repository:
git add -f vendor-private/.nda.toml
python -m ndaguard git install-hooks
python -m ndaguard git untrack # plan
python -m ndaguard git untrack --apply # keep local files
python -m ndaguard check --staged
python -m ndaguard check --history
Pre-commit checks the entire prospective commit against working, staged, and HEAD policies using index bytes. Pre-push checks every reachable tree of pushed tips, including deleted files and historical descriptors. Historical restrictions combine conservatively; deleting a descriptor does not release old content. Shallow history, unmerged index entries, symlinks/submodules, and standalone blob/tree refs are refused.
Hooks respect core.hooksPath, refuse to overwrite foreign hooks, and refresh
local ignore rules on checkout/merge. git sync-excludes manually refreshes the
managed .git/info/exclude block. Ignore rules conservatively omit rule exceptions;
the checks apply exact policy. Descriptors are allowed in Git to distribute policy,
so keep confidential content out of them. Force-add descriptors inside ignored
directories. git uninstall-hooks removes owned hooks but leaves ignore rules.
Reinstall hooks after replacing/moving their Python environment.
Untracking preserves local documents but does not remove old commits. To purge:
python -m pip install 'ndaguard[history]'
python -m ndaguard --json git purge
# Execute in a clean, disposable fresh clone. For local cloning use --no-local.
python -m ndaguard git purge --apply --backup ../sensitive-original.bundle
Purge removes every version of selected paths using git-filter-repo, including
unchanged copies. It requires an external, nonexistent backup path, full history,
a clean index/worktree, and a single attached worktree. The fresh-clone safeguard
stays enabled. Local regular documents are preserved; links cannot be preserved.
Purge appends a persistent path/fingerprint deny list to root .nda.json; review
and commit it so other clones inherit protection. Collected agreement IDs are
associated conservatively with that deny list. Existing descriptor rules remain.
The backup contains original NDA content. Rewriting changes commit IDs and may
invalidate signatures; git-filter-repo normally removes origin. No automatic
push occurs. Remote copies, old clones, caches, and hosting-provider retention
require separate handling. See git-filter-repo's documentation.
Local hooks are bypassable, so require python -m ndaguard check --history in
release/merge CI after fetching full history. CI can install a pinned release
with python -m pip install ndaguard==0.1.0. For hostile contributions, use a trusted checker and policy
outside the proposed changes. CI cannot retract content already uploaded.
See Git hooks and ignore rules.
All commands support global --json. Exit codes: 0 success, 1 Git policy
violation, 2 invalid policy/path or operational failure. Use --help for
inventory filters and remaining options.
Development
Clone or open this standalone project, then create a virtual environment:
python -m venv .venv
Activate it on Linux with source .venv/bin/activate, or in PowerShell with
.\.venv\Scripts\Activate.ps1. On systems where Python is named python3, use
python3 -m venv .venv; the activated environment provides python.
python -m pip install -e '.[dev,history]'
python -m ruff check .
python -m ruff format --check .
python -m pytest -q
python -m build
python -m twine check dist/*
Source lives in src/ndaguard/. Tests run against the installed package, and use
synthetic content and disposable repositories, including real hooks, history
rewriting, and backup recovery. GitHub Actions runs the suite on Ubuntu and Windows
with Python 3.11 through 3.14, and validates the release distributions.
Publishing
The package is prepared for PyPI but must be uploaded before pip install ndaguard
works. The release version has one source: src/ndaguard/_version.py. Update it
and CHANGELOG.md for subsequent releases; use a new version for every upload.
From a clean checkout, install .[dev,history], run the checks above, and build
into an empty dist/ directory. The build creates a source archive and a universal
Python wheel, including the MIT license and py.typed marker.
First upload to TestPyPI:
python -m twine upload --repository testpypi dist/*
Check installation in a separate virtual environment:
python -m pip install --index-url https://test.pypi.org/simple/ --no-deps ndaguard==0.1.0
python -m ndaguard --version
ndaguard --help
Then upload the same artifacts to PyPI:
python -m twine upload dist/*
Use your PyPI account's API token when Twine prompts for credentials (username
__token__). TestPyPI and PyPI have separate accounts and tokens. Check that you
can register the ndaguard name before the first release. See the
Python Packaging User Guide
for account setup and upload details.
Metadata
Release files for ndaguard 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ndaguard-0.1.0.tar.gz | 35.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ndaguard-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 62.6 kB
Release files / ndaguard-0.1.0.tar.gz
| Download URL | ndaguard-0.1.0.tar.gz |
|---|---|
| Size | 35.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
55720d5d94bc56191d42fa7e10e2e1dea196dd6d4df5210ae7129d4f85739624
|
|
BLAKE2b-256 checksum How to use checksums |
a1978d3acc97469ad679ecade501ca22c0b56f43b60c5cef1d3950bc7732ac6a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / ndaguard-0.1.0-py3-none-any.whl
| Download URL | ndaguard-0.1.0-py3-none-any.whl |
|---|---|
| Size | 26.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0556b0e2c3f756dc201cb4d2ed813167b4c7839176749d281267501f3fb13ffa
|
|
BLAKE2b-256 checksum How to use checksums |
19cacfd9e32e1bde4d5d61ab0fea294d809313d3a0caa360793dc861aec4d179
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|