py-ninja-vault: Recursive Directory Encryption with GnuPG
py-ninja-vault is a Python CLI tool that walks a directory tree and applies GnuPG operations to every file — encrypting, decrypting, signing, or verifying — while silently skipping hidden paths. It is designed for developers and system administrators who need to protect sensitive directories from a single command.
Architecture Overview
The project is structured around a single Python package (ninja_vault) and a CLI entry point (py-njv):
ninja_vault/__init__.py— Core library: file iteration, GPG wrappers, configuration helpersbin/py-njv— CLI entry point, argument parsing, dispatches to the library~/.py_njvrc— Per-user configuration file holding the default GPG key ID~/.gnupg/gpg.conf— Standard GnuPG config used to resolve thedefault-key
The library delegates all cryptographic operations to the python-gnupg module, which is a thin wrapper around the system gpg binary. Progress bars are rendered via tqdm when available.
Installation
Clone the repository and install with pip:
git clone https://github.com/flasomm/ninja-vault.git
cd ninja-vault
pip install .
This installs the py-njv command globally and pulls in python-gnupg and tqdm as dependencies.
⚠️ GnuPG 2.x must be available in your
PATH. GnuPG 1.x may work for some operations but is not officially supported.
Configuration
Create ~/.py_njvrc and set your key identifier:
[DEFAULT]
UseKey=YOUR_KEY_ID
Alternatively, pass -K <id> on the command line to override the config value, or use -D to read default-key from ~/.gnupg/gpg.conf.
The config file is only required when neither
-Knor-Dis provided.
Core Library API
All public functions follow the same pattern: validate the target directory, reload the GPG agent, prompt for the passphrase, then iterate over files with an optional progress bar.
encrypt_dir
Recursively encrypts every non-hidden file under dir_to_encrypt. Each file is replaced by a .gpg counterpart; the original is removed unless --no-delete is set.
from ninja_vault import encrypt_dir
encrypt_dir(
dir_to_encrypt='/path/to/secrets',
key_id='A1B2C3D4', # or None to read from ~/.py_njvrc
home_dir=None, # defaults to ~
delete_original=True, # remove plaintext after encryption
dry_run=False, # set True to preview without writing
verbose=False,
)
Hidden files and directories (names starting with
.) are always skipped.
decrypt_dir
Mirrors encrypt_dir. It targets *.gpg files and writes the decrypted output alongside them, then removes the .gpg file unless --no-delete is active.
from ninja_vault import decrypt_dir
decrypt_dir(
dir_to_decrypt='/path/to/secrets',
key_id=None,
home_dir=None,
delete_original=True,
dry_run=False,
verbose=False,
)
sign_dir and verify_dir
sign_dir creates a detached .sig signature for every file. verify_dir then walks the same tree and verifies each .sig file, raising ValueError on failure.
from ninja_vault import sign_dir, verify_dir
sign_dir('/path/to/releases', key_id='A1B2C3D4')
verify_dir('/path/to/releases')
Signatures are detached — the original files are not modified.
Internal Helpers
A few noteworthy private utilities power the library:
def _is_hidden(path: str) -> bool:
"""Return True if any path component starts with '.'."""
return any(part.startswith('.') for part in path.split(os.sep) if part)
def _sanitize_gpg_stderr(stderr: str) -> str:
"""Strip key identity lines from GPG output before printing."""
...
def _reload_agent() -> None:
"""Run gpgconf --reload gpg-agent; silently skipped if gpgconf is absent."""
...
_sanitize_gpg_stderr is a subtle security measure: it removes lines containing key identities (e.g. encrypted with … <email>) so that key metadata is not leaked to terminal logs.
CLI Usage
# Encrypt a directory
py-njv -e /path/to/secrets
# Decrypt a directory
py-njv -d /path/to/secrets
# Sign all files
py-njv --sign /path/to/releases
# Verify all signatures
py-njv --verify /path/to/releases
# Preview without writing (dry run)
py-njv -e /path/to/secrets --dry-run
# Keep source files after encryption
py-njv -e /path/to/secrets --no-delete
# Use a specific key inline
py-njv -e /path/to/secrets -K A1B2C3D4
# Use an alternate GPG home
py-njv -e /path/to/secrets -g /home/ci-user
Always run with
--dry-runfirst on directories you have never encrypted before.
Security Notes
| Behaviour | Detail |
|---|---|
| Source file removal | Performed with a standard os.remove call — not a secure wipe |
| Hidden paths | Skipped unconditionally; no option to include them |
| Key identity leak | Filtered from GPG stderr before printing |
| GPG agent reload | Executed before every operation to flush cached credentials |
Because file removal is not a secure wipe, sensitive data may be recoverable from disk after encryption. For high-security environments, combine py-ninja-vault with a tool like shred or operate on an encrypted filesystem.
Running the Tests
python -m pytest ninja_vault/tests/test_njv.py -v
Test fixtures live in ninja_vault/tests/ alongside the test module, so the suite is fully self-contained.
py-ninja-vault offers a pragmatic, dependency-light solution for bulk GPG operations. Whether you are archiving project secrets before committing to a shared repository or signing release artifacts in a CI pipeline, a single command is all it takes.
Have fun !