Capability gateways for the AI agent era. Give your AI agent the ability to call sensitive commands without giving it the secrets.
中文版 · DESIGN.md · USER GUIDE
cmdseal turns a command template plus secret values into a standalone
macOS binary. The binary can execute the exact command it was sealed
with — and only that command — without revealing the secret, even to
the user who runs it.
command template ─┐
secret values ────┼─► AEAD-sealed runner (C) ──► ad-hoc signed binary
│ │
└── AES-256 key K ──► login.keychain ACL bound to THIS binary's cdhash
Use case: you want an on-device AI agent (or a scripted pipeline) to be
able to call zhmm_cmd --pwd ... --search <term> without the agent
ever seeing the master password.
- ✅ CLI end-to-end working (macOS 13+, tested on 14 / 15 / 26)
- ✅ GUI (PySide6) seal wizard — optional, launches from the same repo
- ✅
.appbundle viamake app(PyInstaller) ⚠️ macOS only for now (Linux / Windows are future work)⚠️ Distributed as source; no Developer ID signed release yet
- macOS (Apple Silicon or Intel; tested on darwin 24+)
- Xcode Command Line Tools (
cc,codesign) /usr/bin/security(ships with macOS)- Python 3.11+ (CLI only uses stdlib)
uv— for the GUI /.appbuild (skip if you only use the CLI)
Generate from Command wizard — turn a working command into a sealable template.
If you prefer clicking:
git clone https://github.com/szgenle/cmdseal.git
cd cmdseal
make sync # install PySide6 into a local uv venv
make app # produces dist/cmdseal.app
open dist/cmdseal.appThis launches the seal wizard — a four-step GUI:
- Command — type your command (e.g.
7zz a -tzip -mem=AES256 -pmypassword {{arg:1}}) - Secrets — if you used
{{secret:NAME}}, fill them in here (skipped otherwise) - Options — output path, label, signing identity
- Execute — preview and run
cmdseal.pyin the background
The GUI is a thin wrapper over cmdseal.py; no crypto code is
duplicated. After sealing, you get a standalone binary that can be
handed to an AI agent or scripted pipeline.
The main window also offers two other entry points:
- Generate from Command... — if you already have a working command,
the wizard auto-detects argument positions and generates a sealable
template with
{{arg:N}}placeholders, saving you from manual rewriting. - Manage Runners... — zero-popup listing of all sealed runners on this machine (metadata read from the keychain). Supports right-click to edit the template (when it contains no secret placeholders) or cascading delete (cleans up both the keychain entry and the disk file).
The first time you run a freshly-sealed binary, macOS shows one
system dialog (login password + "Always Allow"). This is the
partition-list handshake binding the keychain item to this binary's
cdhash. It happens once per sealed binary per user; cmdseal itself
never asks for a password again. All subsequent runs are silent and
millisecond-fast.
For CI pipelines, AI-agent build scripts, or when you prefer the
terminal, cmdseal.py provides full CLI access:
# Seal an encrypted-zip command using WinZip AES-256 (not legacy ZipCrypto).
# 7-Zip ships with macOS via Homebrew: `brew install sevenzip`
# The password placeholder must occupy its own argv position, so we wrap
# the call in `sh -c` to concatenate `-p<pwd>` at runtime.
python3 cmdseal.py seal \
--command 'sh -c "7zz a -tzip -mem=AES256 -p\"$1\" \"$2\" \"$3\"" _ {{secret:zippw}} {{arg:1}} {{arg:2}}' \
--output ./seal_zip
# → two password prompts (value + confirm), then ./seal_zip is built
# Use it — just two positional args now, no password on the command line.
./seal_zip out.zip /path/to/secret.txtGUI users: run
make appinstead; this CLI is for scripting, CI pipelines, and audits.
| Placeholder | Resolved at | Source |
|---|---|---|
{{secret:NAME}} |
seal time | prompted, baked into AEAD ciphertext |
{{arg:N}} |
runtime | argv[N] of the generated binary |
| any other token | — | passed through verbatim |
Placeholders must occupy a whole argv position. Mixed tokens like
"--pwd={{secret:x}}" are rejected; split them:
--pwd {{secret:x}}.
If you write a bare program name (e.g. zip), cmdseal seal resolves
it to an absolute path via shutil.which at seal time and bakes that
path in. The runner refuses $PATH lookup (see §Security model).
The repo ships demo/demo_zip_input.txt
as sample input; the flow below is copy-paste runnable on your
machine:
# 1) Seal an encrypted-zip command (WinZip AES-256) as demo/seal_zip.
# You will be prompted twice for the value of `zippw` (say 'hunter2'):
python3 cmdseal.py seal \
--command 'sh -c "7zz a -tzip -mem=AES256 -p\"$1\" \"$2\" \"$3\"" _ {{secret:zippw}} {{arg:1}} {{arg:2}}' \
--output ./demo/seal_zip
# 2) Use it to compress demo/demo_zip_input.txt into a
# password-protected /tmp/out.zip:
./demo/seal_zip /tmp/out.zip ./demo/demo_zip_input.txt
# First run: ONE macOS partition-list dialog
# (login password + "Always Allow"). A one-time cost to bind the
# keychain item to this binary's cdhash. Subsequent runs: silent,
# sub-second.
# 3) Verify the archive really is password-protected:
unzip /tmp/out.zip -d /tmp/out
# → [/tmp/out.zip] demo_zip_input.txt password: ← requires hunter2
# 4) Verify the password was NOT baked into the binary in plaintext:
strings ./demo/seal_zip | grep -F 'hunter2' && echo FAIL || echo 'PASS: secret not in strings'The payoff: step 2's ./demo/seal_zip can be handed to an on-device
AI agent or a scripted pipeline. Neither can read zippw, and
neither can bypass the keychain ACL to fetch it directly:
# An agent trying to exfiltrate via the CLI:
/usr/bin/security find-generic-password -s cmdseal.<hash>.K -w
# → GUI approval prompt the agent cannot click through. Denied.If you forgot the
cmdseal.<hash>.Kservice name printed at seal time, recover it withstrings ./demo/seal_zip | grep cmdseal.
python3 cmdseal.py rotate ./seal_zip
# Generates a fresh AES-256 key, rewrites the AEAD ciphertext,
# re-signs the binary, swaps the keychain item atomically.
# No user interaction — runs silently in ~1s.make sync # install PySide6 into a local uv venv
make run # launches the main window (development mode)
make app # produces dist/cmdseal.app (standalone)
open dist/cmdseal.appThe GUI is a thin wrapper over cmdseal.py; no crypto code is
duplicated. See gui/ for the sources.
| Entry point | When to use |
|---|---|
| Generate from Command... | You have a working command and want a template with placeholders auto-generated. |
| Advanced Seal... | You already have a template with {{secret:NAME}} / {{arg:N}} and want to seal it. |
| Manage Runners... | Review, edit, or delete existing sealed binaries and their keychain entries. |
Press ⌘, (comma) to open Preferences and optionally set a default
output directory so the wizard always defaults to a fixed folder. The
window size is also persisted across restarts.
- Secret exfiltration via the sealed binary's argv or
ps— the secret is in AEAD ciphertext embedded in the binary; it only exists in decrypted form inside the runner's address space for the brief window between keychain fetch andexecv. - A different process (even the same user) reading the keychain
item — macOS partition-list / ACL bind the item to this exact
binary's cdhash. Verified against
/usr/bin/security, unrelated ad-hoc signed probes, and bitwise-identical copies — all blocked. - PATH-based program substitution — the runner uses
execvwith an absolute path baked in at seal time (v1.1 #2); there is no runtime$PATHlookup. - Dylib injection via environment — the runner strips
DYLD_*andLD_*from its environment beforeexecv(v1.1 #3), and is signed withcodesign --options runtime(v1.1 #4) so dyld itself ignores those variables for this binary.
- A root attacker, or any code running as root (can read any process memory, any keychain).
- A process running as the same user that gains arbitrary code execution and debugs / dumps the running sealed binary.
- Tampering with the generating machine before the binary was built.
- Snapshot / side-channel attacks on the target command (e.g.
zipitself writing the password to some log; that'szip's responsibility, not ours). - Anything on Linux or Windows.
Be honest with yourself about the threat model before relying on
cmdseal. It is a capability gateway, not a vault.
Earlier versions of this README used zip -P (legacy ZipCrypto) in the
seal_zip demo. ZipCrypto is broken: a known-plaintext attack
(bkcrack) recovers the internal
keys in seconds-to-minutes on a laptop once the attacker has any ~12
bytes of plaintext from the archive — and most attachments (PNG, PDF,
JSON, Markdown) have predictable headers, so the attacker often does
not even need to ask for plaintext.
cmdseal protects the password from the agent, but cannot protect the
archive if the archive itself uses a broken cipher. The demo now uses
WinZip AES-256 (7zz -tzip -mem=AES256), which is the cipher
recommended whenever the resulting archive may be transported, shared,
or stored outside the original machine.
See DESIGN.md for the full threat model, the partition-list empirical findings, and the rationale behind Plan D (the current scheme).
# Inspect the keychain entry for a sealed binary (metadata only):
security find-generic-password -s cmdseal.<hash>.K
# Remove the entry (e.g. when retiring a binary):
security delete-generic-password -s cmdseal.<hash>.K
# Inspect the sealed binary's metadata (no secrets visible):
strings ./seal_zip | grep cmdseal
# List all known runners (JSON output for scripting):
python3 cmdseal.py list --jsonYou can also use the GUI's Manage Runners... entry point for a visual overview with one-click delete.
We do not ship pre-built binaries. The sealing model is
per-machine (cdhash + your login keychain), so a binary built on
someone else's machine would be useless to you anyway. To use
cmdseal:
git clonethe repo,- audit what you want to audit,
make appif you want a GUI, or usecmdseal.pydirectly for scripting.
If you want Developer-ID-signed notarized .app releases on GitHub,
open an issue —
it is gated on the maintainer joining the Apple Developer Program.
- First-run prompt on each sealed binary. See §Quick start.
- No registry of which
cmdseal.<hash>.Kservice belongs to which binary beyondstrings <binary> | grep cmdseal. - No automatic cleanup of keychain items when a sealed binary is
deleted. Run
python3 cmdseal.py gc --dry-runto audit orphans, then re-run without--dry-runto reap them. (GUI integration into Manage Runners... is planned.) - No argument whitelisting —
{{arg:N}}is passed to the target command verbatim. If the target command is picky about what values it accepts, that is on the target. - macOS only. Linux (
libsecret) / Windows (DPAPI) are future work; no ETA.
MIT — do what you want, attribution appreciated, no warranty.
cmdseal uses PySide6 and
Qt 6, both under the
LGPL-3.0 license.
Qt is a trademark of The Qt Company Ltd.; cmdseal is not affiliated
with or endorsed by The Qt Company.
See THIRD_PARTY_LICENSES.md for the
full list of third-party components and our LGPL compliance notes.
- DESIGN.md — architecture and design decisions
- DESIGN.zh.md — 中文设计说明
- Author: lioesquieu