Encrypting Code

protect and run, expiry, a stronger two-factor lock, and portable artifacts that don't need a machine lock at all.

magiclock protect turns a .py file into a .pya — an encrypted, gated container — without a compiler and without uploading your source anywhere. magiclock run runs it.

Basic usage

shell
# First run: signs you in and activates this machine, then encrypts.
# Every encrypt after that is instant and non-interactive.
magiclock protect app.py            # -> app.pya
magiclock run app.pya

# Point at a directory to protect a whole project tree recursively
# (.venv/venv/__pycache__/.git/dist/build are skipped automatically).
magiclock protect src/

.pya files are ordinary files you can commit, copy, or ship however you already distribute your app — the encryption travels with the file.

How it works

A .pya is a small versioned container wrapping an encrypted envelope. magiclock run app.pya decrypts it in memory, installs an import hook for anything it imports, and executes it — nothing is ever written back to disk as plaintext. Because the marshaled bytecode inside is tied to the CPython version that encrypted it, a .pya built on Python 3.12 won't load on 3.13; re-run protect after upgrading your interpreter.

Removing the plaintext

shell
magiclock protect app.py --remove   # encrypt, then delete app.py

Expiry: time-limited and self-destructing artifacts

These flags stamp an expiry into the encrypted artifact itself — independent of your license — after which it refuses to decrypt:

shell
magiclock protect app.py --expires-in 30d        # stops decrypting in 30 days
magiclock protect app.py --expires-at 2026-12-31  # stops decrypting on a date
magiclock protect app.py --trial                  # fixed 48-hour self-destruct — for demos and evaluations

--trial, --expires-in, and --expires-at are mutually exclusive — pick one per file.

Stronger: two-factor lock (machine + passphrase)

By default an artifact only needs to be on the right machine to decrypt. Add a passphrase as a second factor:

shell
magiclock protect app.py --lock-passphrase
# prompts for a passphrase (or reads $MAGICLOCK_PASSPHRASE non-interactively)

magiclock run app.pya --passphrase

Portable: no machine lock at all

Sometimes you need an artifact to run on a machine you haven't activated — a customer's box, a machine you don't control. Portable mode drops the machine lock and unlocks with a shared secret instead — weaker, but distributable:

shell
# Passphrase-protected, no machine lock:
magiclock protect app.py --no-bind-machine --passphrase
magiclock run app.pya --passphrase

# Or hand out a generated key instead of a passphrase:
magiclock protect app.py --no-bind-machine --emit-key
magiclock run app.pya --key <the-printed-key>

--lock-passphrase and the portable flags (--no-bind-machine, --passphrase, --emit-key) are mutually exclusive — an artifact is either machine-locked (optionally with a passphrase second factor) or portable (unlocked by passphrase/key instead of machine identity), not both.

For CI and scripted runs, both the passphrase and a portable key can come from the environment instead of a prompt:

shell
export MAGICLOCK_PASSPHRASE=...     # for --lock-passphrase or portable --passphrase artifacts
export MAGICLOCK_DECRYPT_KEY=...    # alternative to `run --key` for --emit-key artifacts
magiclock run app.pya

What's next