Encrypting Code

protect and run, expiry, and the five protection levels — from freely-distributable keyless artifacts to a machine-locked two-factor vault.

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. By default the artifact is portable and keyless: it decrypts transparently on any machine that has magiclock installed, with no account, passphrase, or machine lock on the running side. That makes the default a convenience/obfuscation level — your source doesn't ship in the clear, but anyone with the file and the runtime can execute it. When you need real access control, pick a stronger level below.

Choosing a protection level

LevelFlagsWho can decrypt
Keyless portable (default)(none)Anyone with magiclock installed — convenience/obfuscation grade.
Passphrase portable--passphraseAnyone who knows the passphrase, on any machine.
Key portable--emit-keyAnyone holding the generated key, on any machine.
Machine-locked--bind-machineOnly this activated machine — a non-exportable machine key plus the license gate; the strongest single-factor level.
Machine + passphrase--bind-machine --passphraseThis activated machine and the passphrase — two-factor.

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.

Portable with a secret: passphrase or key

The keyless default asks nothing of whoever runs the artifact. To keep the portability but add an access secret, pick one of two portable modes:

shell
# Passphrase-protected, still runs on any machine:
magiclock protect app.py --passphrase
magiclock run app.pya --passphrase

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

--passphrase and --emit-key are mutually exclusive — pick one secret per artifact.

Strongest: machine lock (--bind-machine)

When the artifact must not leave your machines at all, bind it to this activated machine — decryption then requires this machine's non-exportable vault key and passes the license gate, and the file is useless anywhere else:

shell
magiclock protect app.py --bind-machine
magiclock run app.pya            # only works on the machine that encrypted it

Add a passphrase on top for a two-factor lock — the right machine and a secret:

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

magiclock run app.pya --passphrase

--emit-key is portable-only and mutually exclusive with --bind-machine — a machine-locked artifact's key never leaves the machine, so there is nothing to hand out.

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 --passphrase artifacts (portable or two-factor)
export MAGICLOCK_DECRYPT_KEY=...    # alternative to `run --key` for --emit-key artifacts
magiclock run app.pya

What's next