SAFE Encryption Tool
SkillDev toolsEncrypt, decrypt, and manage keys with the SAFE CLI — a modern GPG alternative with post-quantum support.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the SAFE Encryption Tool skill
What this skill tells your AI
The instructions your AI receives, as published by tan-yong-sheng/ai-vision-mcp in .claude/skills/safe-encryption/SKILL.md and read by ahel’s review.
SAFE is a modern encryption CLI with post-quantum support, multi-recipient encryption, and composable authentication.
Behavior Guidelines
When this skill is loaded:
- Check if
safeis installed (which safe). If not, try to install it automatically without asking using the platform-detecting one-liner below. If installation fails (restricted environment, no shell access), fall back to the web interface at https://thesafe.dev — see "Browser-Based Alternative" below. - When the user asks to encrypt/decrypt, just do it. Don't ask for confirmation.
- If a password is needed and not provided, use
-pwithout a value (prompts interactively or readsSAFE_PASSPHRASEenv var). In automation, use-p env:VARNAMEto read from environment variables. - If a key file is needed, run
safe keysto check existing identities and recipients. If no key exists, runsafe keygen x25519to generate one (auto-stores to~/.safe/). - Always use absolute paths for key files and encrypted files to avoid working directory issues.
- Always pipe instead of writing temp files. When decrypting inline/embedded content (base64, pasted data), pipe directly:
echo "..." | base64 -d | safe decrypt -k key.key. Never write intermediate.safefiles to/tmpor elsewhere. - Bare name recipients work. Use
-r aliceinstead of-r /full/path/to/alice.x25519.pubwhen the recipient is in~/.safe/recipients/. - Decrypt auto-discovers keys. If no
-kor-pis provided,safe decryptautomatically tries all keys in~/.safe/keys/.
Installation
Download the prebuilt binary from thesafe.dev/download:
macOS Apple Silicon:
curl -sL https://thesafe.dev/downloads/safe-darwin-arm64 -o safe && chmod +x safe && sudo mv safe /usr/local/bin/
macOS Intel:
curl -sL https://thesafe.dev/downloads/safe-darwin-amd64 -o safe && chmod +x safe && sudo mv safe /usr/local/bin/
Linux x86_64:
curl -sL https://thesafe.dev/downloads/safe-linux-amd64 -o safe && chmod +x safe && sudo mv safe /usr/local/bin/
Linux ARM64:
curl -sL https://thesafe.dev/downloads/safe-linux-arm64 -o safe && chmod +x safe && sudo mv safe /usr/local/bin/
Auto-detect platform (one-liner):
which safe || { OS=$(uname -s | tr '[:upper:]' '[:lower:]'); ARCH=$(uname -m); \
[ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ] && ARCH=arm64 || ARCH=amd64; \
curl -sL "https://thesafe.dev/downloads/safe-${OS}-${ARCH}" -o safe && chmod +x safe && sudo mv safe /usr/local/bin/; }
Verify installation:
safe --help
Optional: verify checksum (SHA-256 values from checksums.txt):
ddf638ccfd119d9c3351825a2e3f9e3660f754009eb9b106d7bba8cd698c3df4 safe-darwin-arm64
6be4942048a23e99485c0123cf0bb4786613a3dabbcc6927aa478cd0e719ad0b safe-darwin-amd64
85c7822ac0309e62ca29d03394bfe87a506f329f61c30b070c379a6d2f5af815 safe-linux-amd64
33207a4c6bbf85b8299f2f75e18aa6e13f9a8ae4bcee72a9d869034d78ba4cac safe-linux-arm64
Installation Debugging
"safe: command not found" after install:
- Check if binary exists:
ls -la /usr/local/bin/safe - If sudo failed, install to user dir:
mv safe ~/.local/bin/ && export PATH="$HOME/.local/bin:$PATH" - Refresh shell:
hash -ror start new terminal
Permission denied on /usr/local/bin:
- Use sudo:
sudo mv safe /usr/local/bin/ - Or install to user dir:
mkdir -p ~/.local/bin && mv safe ~/.local/bin/
When CLI Is Unavailable — Browser-Based Alternative
If CLI installation fails or isn't possible (restricted environment, no build tools, no shell access, sandboxed IDE, etc.), use the web interface at https://thesafe.dev.
This is a fully functional SAFE implementation running in the browser — not a demo. All cryptographic operations happen client-side. No data leaves the browser.
The web interface supports all core SAFE operations via a single-page layout with these sections:
- ENCRYPT tab: Encrypt data with passwords, public keys, passkeys, or GitHub usernames
- DECRYPT tab: Decrypt SAFE messages with passwords, private keys, passkeys, or GitHub keys
- KEYCHAIN section: Save, import, export, and manage keys and passwords
- ADVANCED section: Lock Management (add/remove recipients), Re-encrypt Demo, Tests
- LOG section: View operation log output
Manual workflow (no automation needed):
Users can interact with the web interface directly:
- Encrypt: Enter plaintext, add recipients (key, password, passkey, or GitHub username), click "ENCRYPT". Copy or download the output.
- Decrypt: Paste/upload/URL-load a SAFE message, add credentials (private key, password, passkey, or GitHub), click "DECRYPT". Copy or download the plaintext.
Generated keys are automatically saved in the KEYCHAIN section and can be reused across operations.
Agent with MCP browser tools (Playwright, Puppeteer, etc.):
If you have access to browser automation tools (e.g., Playwright MCP server, Claude in Chrome, Puppeteer MCP), you can drive the web interface directly.
Key behaviors to know:
- After encrypting, output auto-populates into the decrypt section's SAFE message input
- The browser auto-matches saved credentials and pre-adds them to decrypt
- Generated keys are auto-saved to the Credentials section (04)
- Always take a snapshot (
browser_snapshot) after each action to get updated element references
ARIA labels for automation:
The interface uses semantic ARIA roles throughout:
| Element | ARIA Label | Role |
|---|---|---|
| KEM type selector | "Select key encapsulation mechanism type" | combobox |
| Generate button | "Generate new keypair with selected KEM type" | button |
| Plaintext input | "Enter plaintext message to encrypt" | textbox |
| Add Step button | "Add encryption step to recipient path" | button |
| Step type selector | "Select encryption step type" | combobox |
| Password field (encrypt) | "Enter password for encryption step" | textbox |
| Confirm step | "Confirm encryption step" | button |
| Encrypt button | "Encrypt plaintext with configured settings and recipient path" | button |
| Encrypted output | "Encrypted SAFE message output" | textbox |
| SAFE message input | "Paste encrypted SAFE message to decrypt" | textbox |
| Add credential button (decrypt) | "Add credential to decryption attempt" | button |
| Add credential button (keychain) | "Add credential to keychain" | button |
| Add all keychain button | "Add all keychain entries as credentials" | button |
| Credential type selector | "Select credential type" | combobox |
| New Passkey menu item | "Create a new passkey" | menuitem |
| Password field (decrypt) | "Enter password for decryption" | textbox |
| Confirm credential | "Confirm credential" | button |
| Decrypt button | "Decrypt SAFE message using provided keychain" | button |
| Decrypted output | "Decrypted plaintext message" | textbox |
| Copy buttons | "Copy encrypted SAFE message to clipboard" / "Copy decrypted plaintext to clipboard" | button |
| Download buttons | "Download encrypted SAFE message as file" / "Download decrypted file" | button |
| Share button (output) | "Share encrypted SAFE message via URL" / "Share decrypted output via URL" | button |
| Send button (output) | "Send encrypted output over WebRTC" | button (encrypted output only) |
| Clear button (output) | "Clear encrypted output" / "Clear decrypted output" | button |
| Share button (keychain) | "Share public key via URL" | button |
| Label button (keychain) | "Rename key label" | button |
| Use File toggles | "Use file instead of plaintext input" / "Use file instead of SAFE message input" | generic (clickable) |
| Navigation links | New (#keygen), Encrypt (#encrypt), Decrypt (#decrypt), Keychain (#keyring), Advanced (expandable) | link |
| Advanced sections | #unlock, #reencrypt, #tests, #log | link (under Advanced dropdown) |
| Sections | role="region" with labels like "01 / Key Generation" | region |
| Log output | "Activity log showing operations and their results" | log |
Note on Advanced navigation: The Advanced sections (#unlock, #reencrypt, #tests, #log) are accessed via an "Advanced" navigation item that expands to show these additional features.
Note on terminology: The UI currently uses mixed terminology - Section 04 is labeled "Keychain" and the decrypt button references "keychain", but the decrypt section's credential management buttons still use "Credentials" in some ARIA labels (e.g., "Add all keychain entries as credentials"). Both terms refer to the same saved keys/passwords.
Keychain shortcut buttons:
Each saved key in Section 04 (Keychain) has quick action buttons:
- Enc: Adds the public key as an encryption recipient step (one click — skips the Add Step → select type → paste → OK workflow)
- Dec: Adds the private key as a decrypt credential (one click — skips the Add → select type → paste → OK workflow)
- PUB: Shows/copies the public key
- PRIV: Shows/copies the private key
- Share: Generates a shareable URL for the public key
- Label: Rename the key for easier identification
- Del: Removes the key from the keychain
Prefer using Enc/Dec shortcuts over the manual Add Step flow when keys are saved in the keychain — it reduces 4 interactions to 1.
File upload:
Both encrypt and decrypt sections have a "Use File" toggle. Clicking it triggers a file chooser dialog. With MCP Playwright, use browser_file_upload to provide the file path. Note: file paths must be within the MCP server's allowed directories.
Example: Encrypt with password (MCP Playwright)
# 1. Navigate
browser_navigate(url="https://thesafe.dev")
browser_snapshot()
# 2. Type plaintext (use ref from snapshot for "Enter plaintext message to encrypt")
browser_type(ref=<plaintext-ref>, text="secret data")
# 3. Add password step
browser_click(ref=<add-step-button-ref>) # "Add encryption step to recipient path"
browser_snapshot() # Get refs for step config form
# 4. Select Password type (default may be "Public Key")
browser_select_option(ref=<step-type-ref>, values=["Password"]) # "Select encryption step type"
browser_snapshot() # Get password field ref
# 5. Enter password
browser_type(ref=<password-ref>, text="my-password") # "Enter password for encryption step"
# 6. Confirm the step
browser_click(ref=<ok-ref>) # "Confirm encryption step"
# 7. Encrypt
browser_click(ref=<encrypt-ref>) # "Encrypt plaintext with configured settings..."
browser_snapshot() # Output is in "Encrypted SAFE message output" textbox
# Optional: Share or clear the output
# browser_click(ref=<share-button-ref>) # "Share encrypted SAFE message via URL"
# browser_click(ref=<clear-button-ref>) # "Clear encrypted output"
Example: Encrypt with saved key (fastest path)
# 1. Navigate
browser_navigate(url="https://thesafe.dev")
browser_snapshot()
# 2. Type plaintext
browser_type(ref=<plaintext-ref>, text="secret data")
# 3. Click "Enc" on a saved key in Credentials section (one click adds recipient)
browser_click(ref=<enc-button-ref>)
# 4. Encrypt
browser_click(ref=<encrypt-ref>)
browser_snapshot()
Example: Decrypt from cold (no auto-populated credentials)
# 1. Paste SAFE message into decrypt input
browser_type(ref=<safe-message-ref>, text="-----BEGIN SAFE UNLOCK-----\n...")
# 2. Add credential
browser_click(ref=<add-credential-ref>) # "Add credential to decryption attempt"
browser_snapshot()
# 3. Select Password type (default is "Private Key")
browser_select_option(ref=<credential-type-ref>, values=["Password"])
browser_snapshot()
# 4. Enter password
browser_type(ref=<password-ref>, text="my-password")
# 5. Confirm credential
browser_click(ref=<confirm-ref>) # "Confirm credential"
# 6. Decrypt
browser_click(ref=<decrypt-ref>) # "Decrypt SAFE message using provided credentials"
browser_snapshot() # Output is in "Decrypted plaintext message" textbox
Example: Same-session encrypt→decrypt (auto-populated)
After encrypting, the output auto-populates into the decrypt section. If the matching key is saved in credentials, it auto-adds the private key. Just click Decrypt — no manual credential entry needed.
Programmatic browser automation (standalone scripts):
For non-MCP environments, use Playwright or Puppeteer directly:
# Example with Playwright (Python)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto('https://thesafe.dev')
# Generate X25519 keypair
page.get_by_role("combobox", name="Select key encapsulation mechanism type").select_option("X25519")
page.get_by_role("button", name="Generate new keypair").click()
# Encrypt with password
page.get_by_role("textbox", name="Enter plaintext message to encrypt").fill("secret message")
page.get_by_role("button", name="Add encryption step to recipient path").click()
page.get_by_label("Select encryption step type").select_option("Password")
page.get_by_role("textbox", name="Enter password for encryption step").fill("mypassword")
page.get_by_role("button", name="Confirm encryption step").click()
page.get_by_role("button", name="Encrypt plaintext").click()
# Read encrypted output
encrypted = page.get_by_label("Encrypted SAFE message output").input_value()
# Decrypt (message and credentials auto-populate from encrypt)
page.get_by_role("button", name="Decrypt SAFE message").click()
decrypted = page.get_by_label("Decrypted plaintext message").input_value()
print(f"Decrypted: {decrypted}") # "secret message"
browser.close()
Multi-Recipient Encryption:
Both the browser UI and CLI support encrypting for multiple recipients. Each recipient can decrypt the message independently using their own credential.
Browser workflow:
- Configure first recipient in "Recipient 1" (password or public key)
- Click "+ Add Recipient" button
- Configure second recipient in "Recipient 2"
- Repeat for additional recipients (no limit)
- Click "Encrypt" - message is encrypted once but decryptable by any recipient
How it works:
- Each recipient gets their own UNLOCK block in the SAFE message
- File is encrypted once with a symmetric key
- Symmetric key is wrapped separately for each recipient
- Any recipient can decrypt using their credential (password or private key)
- Recipients cannot see who else has access
CLI multi-recipient examples:
# Encrypt for multiple recipients using -r flag multiple times
safe encrypt -i file.txt -o file.safe -r alice.pub -r bob.pub -r charlie.pub
# Mix recipient types (password + keys)
safe encrypt -i file.txt -o file.safe -p mypassword -r alice.pub -r bob.pub
# Encrypt for GitHub users (fetches public keys from GitHub)
safe encrypt -i file.txt -o file.safe -r github:grittygrease
# Multiple GitHub users
safe encrypt -i file.txt -o file.safe -r github:alice -r github:bob
# Encrypt for GitHub users and a password
safe encrypt -i file.txt -o file.safe -p teampassword -r github:alice -r github:bob
GitHub username recipient (github:username):
- Fetches SSH public keys from
https://github.com/{username}.keys - Automatically converts p-256 and x25519 keys to SAFE format
- Both key types are added as separate recipients if available
- Requires user to have public keys on their GitHub profile
- Error if no keys found:
github:username: no keys found
Example output:
$ safe encrypt -i test.txt -o test.safe -r github:grittygrease
# Creates UNLOCK blocks for both p-256 and x25519 keys from GitHub
$ safe info -i test.safe
LOCK Blocks: 2
[0] hpke(kem=p-256,id=QyLFP/...)
[1] hpke(kem=x25519,id=r1VeL...)
Agent-to-Agent Communication via GitHub Gist:
Agents can securely exchange encrypted messages using GitHub Gist as a transport layer. This enables asynchronous, persistent communication between agents with different GitHub accounts.
Complete Workflow:
Agent A (Sender):
# 1. Create message for Agent B
echo "Task completed. Results attached." > message.txt
# 2. Encrypt for Agent B's GitHub account
safe encrypt -i message.txt -o message.safe -r github:agentb-username
# 3. Upload encrypted message to public Gist
gh gist create message.safe --desc "Encrypted message for agentb-username" --public
# Output: https://gist.github.com/agenta-username/{gist-id}
Agent B (Receiver):
# Method 1: Direct pipe (simplest, auto-discovers keys)
curl -sL https://gist.github.com/alice/{gist-id}/raw | safe decrypt
# Method 2: Download, inspect, then decrypt
curl -sL https://gist.github.com/alice/{gist-id}/raw > received.safe
safe info -i received.safe # Verify sender and encryption details
safe decrypt -i received.safe -o message.txt
# Method 3: Explicit key (if auto-discovery doesn't work)
curl -sL https://gist.github.com/alice/{gist-id}/raw | safe decrypt -k ~/.safe/keys/bob.x25519.key
SSH Key Auto-Discovery (SAFE CLI v2.3+):
The SAFE CLI automatically discovers and uses SSH private keys from ~/.ssh/:
- ✅ Ed25519 keys → converted to X25519
- ✅ P-256 ECDSA keys → used directly
- ✅ Unencrypted keys only (passphrase-protected keys silently skipped)
- ✅ Zero configuration - just works if your SSH keys match GitHub public keys
Auto-Discovery Order:
~/.safe/keys/*.key- Native SAFE format keys (checked first)~/.ssh/*- All SSH private keys in~/.ssh/directory- Ed25519 keys → converted to X25519
- P-256 ECDSA keys → used directly
Example Auto-Discovery Output:
$ curl -sL https://gist.github.com/.../raw | safe decrypt
safe: using SSH key ~/.ssh/id_ed25519
safe: trying 3 key(s) (2 native + 1 SSH)
[decrypted message]
Key Requirements:
- Agent B must have private keys that correspond to the public keys on their GitHub profile
- GitHub SSH keys must be added to
https://github.com/{username}.keys - Private keys can be in
~/.safe/keys/(SAFE format) OR~/.ssh/(OpenSSH format) - Gist can be public (encrypted content is safe) or private for additional obscurity
Multi-Agent Broadcast:
# Encrypt for multiple agents
safe encrypt -i broadcast.txt -o broadcast.safe \
-r github:agent1 \
-r github:agent2 \
-r github:agent3
# Any of the three agents can decrypt independently
gh gist create broadcast.safe --desc "Team update" --public
Agent Identity Setup:
To enable decryption, agents need to set up their GitHub SSH keys and store private keys:
# Option 1: Use existing SSH keys (simplest - zero setup!)
# If you already have ~/.ssh/id_ed25519 or ~/.ssh/id_ecdsa uploaded to GitHub, you're done!
# SAFE CLI auto-discovers SSH keys - no key generation needed
# Option 2: Generate new SSH key and upload to GitHub
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -C "safe-agent-key"
gh ssh-key add ~/.ssh/id_ed25519.pub --title "SAFE Agent Key"
# Done! SAFE CLI will auto-discover this key
# Option 3: Generate SAFE-native keys (for advanced use cases)
safe keygen x25519 -o agent-id
# Upload agent-id.x25519.pub to https://github.com/settings/keys (manual conversion needed)
mv agent-id.x25519.key ~/.safe/keys/
# Test encryption to self (works with any option above)
safe encrypt -i test.txt -o test.safe -r github:your-username
safe decrypt -i test.safe -o decrypted.txt
# With SSH keys, decryption auto-discovers your keys from ~/.ssh/
Browser-Based Agent Workflow:
Agents using thesafe.dev have full GitHub support for both encryption and decryption.
Encrypt to a GitHub user (browser):
- Go to the ENCRYPT tab
- Enter your message
- Click ADD FACTOR → NEW FACTOR → select GITHUB
- Enter the GitHub username (e.g.,
smithclay) - Click FETCH KEYS — the browser fetches public keys from
https://github.com/{username}.keys - Click ENCRYPT
- Copy the output and share via Gist
Decrypt a GitHub-encrypted message (browser):
- Go to the DECRYPT tab
- Load the encrypted message: paste text, use the FILE button to upload, or use the URL button to load directly from a Gist URL (e.g., paste the raw Gist URL)
- Click ADD → GITHUB → enter your GitHub username → click FETCH KEYS (This matches your public keys to the message's LOCK blocks)
- Click ADD → KEY → paste your SSH private key (from
~/.ssh/id_ed25519or~/.ssh/id_ecdsa) — or use the "Import SSH private key from GitHub" button if available - Click DECRYPT
Recommended approach for browser-based workflows:
- Both CLI and browser support
github:usernamefor encryption equally well - CLI is simpler for decryption — it auto-discovers SSH keys from
~/.ssh/; browser requires pasting the private key once - URL button is convenient — load encrypted Gist content directly without curl
Send encrypted output over WebRTC (browser):
The browser supports real-time peer-to-peer transfer of encrypted SAFE messages via WebRTC — no copy-paste required:
Sender:
- Encrypt your message as usual to produce the encrypted output
- Click Send in the output toolbar (next to Download, Share, Copy)
- A dialog appears: "Share join URL, then wait for receiver..."
- The join URL is offered via the native OS share sheet (if available) or logged in the Log panel
- Keep the tab open — when the receiver connects, the dialog updates to "Receiver connected. Starting transfer..."
- Transfer completes automatically
Receiver:
- Open the join URL:
https://thesafe.dev/?session=<id>&token=<token> - The page auto-connects, receives the encrypted message, and switches to the Decrypt tab with the message pre-loaded
- Add credentials and click Decrypt as normal
Notes:
- Join URLs expire after 30 minutes
- Max transfer size: 100 MB
- Sender must keep the tab open until the receiver connects
- Share (separate button) shares the file or text directly with no server involved; Send is the WebRTC real-time peer flow
Agent Ping/Notification Workflow:
You can "ping" another agent using their GitHub username without needing their public key in advance:
# Alice pings Bob (discovers keys automatically via github:username)
echo "PING: Status update requested" > ping.txt
safe encrypt -i ping.txt -o ping.safe -r github:bob
gh gist create ping.safe --desc "Ping from Alice" --public
# Bob discovers the ping and decrypts (SSH key auto-discovery!)
curl -sL https://gist.github.com/alice/{gist-id}/raw | safe decrypt
# safe: using SSH key ~/.ssh/id_ed25519
# safe: trying 1 key(s) (0 native + 1 SSH)
# PING: Status update requested
# Bob responds back to Alice
echo "PONG: Status OK, task 75% complete" > pong.txt
safe encrypt -i pong.txt -o pong.safe -r github:alice
gh gist create pong.safe --desc "Response to Alice" --public
Key Benefits:
- ✅ No prior key exchange needed -
github:usernamefetches public keys automatically - ✅ No key management needed - reuse existing SSH keys from GitHub
- ✅ Works instantly if you already have SSH keys on GitHub
- ✅ Works for any GitHub user with public SSH keys on their profile
- ✅ Both agents can initiate communication
- ✅ Asynchronous - sender doesn't need to wait for response
- ✅ Persistent - messages remain in Gist until deleted
Discovery Methods:
- GitHub Gist notifications (if agent watches their own Gists)
- Periodic polling of GitHub API for new Gists mentioning their username
- GitHub webhooks for real-time notifications
- RSS feeds for public Gists
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 78
- Forks
- 16
- Last commit
- Apr 2026
Advanced
- Catalog kind
- skill
- Gateway key
safe-encryption- Source
- github.com/tan-yong-sheng/ai-vision-mcp