Skip to main content

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. defaults applies 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.
  • *.pdf matches directly in scope; **/*.pdf matches any depth; private/ and private/** cover descendants. *, ?, and character classes are supported.
  • Rule classification defaults to restricted, and include defaults to ["**"]. Public rules can independently set deny_git or deny_bundle to true.
  • 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)

Source distribution for ndaguard 0.1.0
File Size Uploaded
ndaguard-0.1.0.tar.gz 35.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ndaguard 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page