Skip to content

Instantly share code, notes, and snippets.

@le-dawg
Created May 19, 2026 13:53
Show Gist options
  • Select an option

  • Save le-dawg/d48575aca23984919e4ea9a97f9b1f31 to your computer and use it in GitHub Desktop.

Select an option

Save le-dawg/d48575aca23984919e4ea9a97f9b1f31 to your computer and use it in GitHub Desktop.
localcrypt — macOS system-wide AES-256 text encryption via Services / keyboard shortcuts
#!/bin/bash
# =============================================================================
# localcrypt — macOS system-wide text encryption via Services menu / shortcuts
# =============================================================================
#
# WHAT IT DOES
# ------------
# Adds two entries to macOS Services (right-click menu + keyboard shortcuts):
#
# Encrypt (Cmd+Ctrl+Shift+E)
# Select any text in any app → trigger → text disappears →
# AES-256 ciphertext is in your clipboard → Cmd+V to paste it
#
# Decrypt (Cmd+Ctrl+Shift+D)
# Select ciphertext in any app → trigger → ciphertext is replaced
# by the original plaintext in-place (no Cmd+V needed)
#
# SECURITY MODEL
# --------------
# Your passphrase is stored ONCE in ~/.localcrypt_key (chmod 600).
# It is never entered again after install. Anyone with filesystem access
# to your Mac could read it — this tool prioritises convenience, not
# military-grade security. Good for keeping text private from colleagues
# glancing at your screen or reading shared tools like Productive/Notion.
#
# ENCRYPTION
# ----------
# AES-256-CBC, PBKDF2-SHA256, 100 000 iterations (openssl built into macOS).
# The same passphrase on any Mac can decrypt text encrypted here.
#
# RECOVERY
# --------
# If you lose ~/.localcrypt_key: re-run this script with the same passphrase
# and all previously encrypted text will be decryptable again.
#
# REQUIREMENTS
# ------------
# macOS 13 Ventura or later. No third-party dependencies.
#
# INSTALL
# -------
# curl -fsSL <gist-url> | bash
# or:
# bash localcrypt-install.sh
#
# UNINSTALL
# ---------
# rm -rf ~/.localcrypt ~/.localcrypt_key
# rm -rf ~/Library/Services/Encrypt.workflow ~/Library/Services/Decrypt.workflow
# =============================================================================
set -euo pipefail
# ── Colour helpers ────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
ok() { printf "${GREEN}✓${NC} %s\n" "$*"; }
warn() { printf "${YELLOW}!${NC} %s\n" "$*"; }
die() { printf "${RED}✗${NC} %s\n" "$*" >&2; exit 1; }
echo ""
echo " localcrypt installer"
echo " ════════════════════"
echo ""
# ── Preflight checks ──────────────────────────────────────────────────────────
[[ "$(uname)" == "Darwin" ]] || die "macOS only"
[[ $(sw_vers -productVersion | cut -d. -f1) -ge 13 ]] || die "macOS 13+ required"
command -v openssl &>/dev/null || die "openssl not found"
command -v osascript &>/dev/null || die "osascript not found"
ok "macOS $(sw_vers -productVersion) — all prerequisites met"
# ── Passphrase setup ──────────────────────────────────────────────────────────
KEY_FILE="$HOME/.localcrypt_key"
if [[ -f "$KEY_FILE" ]]; then
warn "Key file already exists at $KEY_FILE — skipping passphrase setup"
warn "(Delete it and re-run to change your passphrase)"
else
echo ""
echo " Choose a passphrase. It will be stored in $KEY_FILE"
echo " and never asked again. If you lose it, re-run this script"
echo " with the same passphrase to recover decryption ability."
echo ""
printf " Enter passphrase: "
read -rs PASS; echo
[[ -n "$PASS" ]] || die "Passphrase cannot be empty"
printf '%s' "$PASS" > "$KEY_FILE"
chmod 600 "$KEY_FILE"
ok "Passphrase saved to $KEY_FILE (chmod 600)"
fi
# ── Deploy scripts to ~/.localcrypt/ ─────────────────────────────────────────
SCRIPT_DIR="$HOME/.localcrypt"
mkdir -p "$SCRIPT_DIR"
# encrypt.sh
# ----------
# Reads selected text from stdin (piped by macOS NSServices mechanism).
# Encrypts with AES-256-CBC using the key file.
# Puts ciphertext in clipboard — original text is cleared by the service.
# Falls back to Cmd+C clipboard capture for apps that don't support NSServices.
cat > "$SCRIPT_DIR/encrypt.sh" << 'ENCRYPT_SCRIPT'
#!/bin/bash
set -uo pipefail
KEY_FILE="$HOME/.localcrypt_key"
# macOS NSServices pipes the selected text to stdin (inputMethod=0 in workflow)
INPUT=$(cat)
if [[ -z "$INPUT" ]]; then
# Fallback: simulate Cmd+C for apps that don't pipe via NSServices stdin
osascript -e 'tell application "System Events" to tell (first application process whose frontmost is true) to keystroke "c" using command down' 2>/dev/null
sleep 0.4
INPUT=$(pbpaste)
fi
if [[ -z "$INPUT" ]]; then
osascript -e 'display notification "No text selected" with title "Encrypt"' 2>/dev/null
exit 0
fi
# Encrypt: AES-256-CBC, PBKDF2-SHA256, 100k iterations, base64 output
# tr -d '\n' collapses multi-line base64 to a single line (avoids rich-text
# editor issues when the ciphertext is pasted into tools like Productive)
ENCRYPTED=""
if ! ENCRYPTED=$(printf '%s' "$INPUT" \
| openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -a \
-pass file:"$KEY_FILE" 2>/dev/null \
| tr -d '\n'); then
osascript -e 'display notification "Encryption failed" with title "Encrypt"' 2>/dev/null
exit 1
fi
# Put ciphertext in clipboard — user pastes it with Cmd+V
printf '%s' "$ENCRYPTED" | pbcopy
osascript -e 'display notification "Encrypted — Cmd+V to paste" with title "Encrypt"' 2>/dev/null
# No stdout: NSServices replaces the selected text with empty (original disappears),
# which acts as visual confirmation that encryption happened
ENCRYPT_SCRIPT
# decrypt.sh
# ----------
# Reads selected ciphertext from stdin.
# Decrypts and outputs plaintext to stdout — NSServices replaces the
# selected ciphertext with the plaintext in-place (no Cmd+V needed).
cat > "$SCRIPT_DIR/decrypt.sh" << 'DECRYPT_SCRIPT'
#!/bin/bash
set -uo pipefail
KEY_FILE="$HOME/.localcrypt_key"
# macOS NSServices pipes selected text to stdin (inputMethod=0 in workflow).
# tr -d '[:space:]' strips any trailing newline/whitespace added by the app.
INPUT=$(cat | tr -d '[:space:]')
if [[ -z "$INPUT" ]]; then
# Fallback: simulate Cmd+C for apps that don't pipe via NSServices stdin
osascript -e 'tell application "System Events" to tell (first application process whose frontmost is true) to keystroke "c" using command down' 2>/dev/null
sleep 0.4
INPUT=$(pbpaste | tr -d '[:space:]')
fi
if [[ -z "$INPUT" ]]; then
osascript -e 'display notification "No text selected" with title "Decrypt"' 2>/dev/null
exit 0
fi
# Decrypt: must match encrypt.sh parameters exactly
# printf '%s\n' adds a trailing newline — openssl's base64 decoder requires it
DECRYPTED=""
if ! DECRYPTED=$(printf '%s\n' "$INPUT" \
| openssl enc -d -aes-256-cbc -pbkdf2 -iter 100000 -a \
-pass file:"$KEY_FILE" 2>/dev/null); then
osascript -e 'display notification "Decryption failed — wrong key or corrupted text" with title "Decrypt"' 2>/dev/null
exit 1
fi
# Also put plaintext in clipboard as a convenience backup
printf '%s' "$DECRYPTED" | pbcopy
osascript -e 'display notification "Decrypted" with title "Decrypt"' 2>/dev/null
# stdout: NSServices replaces the selected ciphertext with this output,
# so the plaintext appears in-place without needing Cmd+V
printf '%s' "$DECRYPTED"
DECRYPT_SCRIPT
chmod +x "$SCRIPT_DIR/encrypt.sh" "$SCRIPT_DIR/decrypt.sh"
ok "Scripts installed to $SCRIPT_DIR"
# ── Deploy Automator Quick Action workflows ───────────────────────────────────
# These are macOS Services (Quick Actions) that appear in the right-click menu
# and can be assigned keyboard shortcuts. Each workflow runs a single
# "Run Shell Script" action that calls our script above.
#
# Key workflow settings:
# workflowTypeIdentifier = com.apple.Automator.servicesMenu → it's a Service
# serviceInputTypeIdentifier = com.apple.Automator.text → text input
# inputMethod = 0 → pipe selected text to script stdin (NOT as arguments)
# NSReturnTypes = NSStringPboardType → stdout replaces the selected text
SERVICES_DIR="$HOME/Library/Services"
mkdir -p "$SERVICES_DIR"
# Remove old versions if present
rm -rf "$SERVICES_DIR/Encrypt.workflow" \
"$SERVICES_DIR/Decrypt.workflow" \
"$SERVICES_DIR/EncryptForAIR.workflow" \
"$SERVICES_DIR/DecryptForAIR.workflow"
write_workflow() {
local name="$1" # "Encrypt" or "Decrypt"
local script="$2" # encrypt.sh or decrypt.sh
local uuid_prefix="$3"
local dir="$SERVICES_DIR/${name}.workflow/Contents"
mkdir -p "$dir"
# document.wflow — the Automator workflow definition
cat > "$dir/document.wflow" << WFLOW
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AMApplicationBuild</key>
<string>521.1</string>
<key>AMApplicationVersion</key>
<string>2.10</string>
<key>AMDocumentVersion</key>
<string>2</string>
<key>actions</key>
<array>
<dict>
<key>action</key>
<dict>
<key>AMAccepts</key>
<dict>
<key>Container</key>
<string>List</string>
<key>Optional</key>
<true/>
<key>Types</key>
<array>
<string>com.apple.cocoa.string</string>
</array>
</dict>
<key>AMActionVersion</key>
<string>2.0.3</string>
<key>AMApplication</key>
<array>
<string>automator</string>
</array>
<key>AMParameterProperties</key>
<dict>
<key>COMMAND_STRING</key>
<dict/>
<key>CheckedForUserDefaultShell</key>
<dict/>
<key>inputMethod</key>
<dict/>
<key>shell</key>
<dict/>
<key>source</key>
<dict/>
</dict>
<key>AMProvides</key>
<dict>
<key>Container</key>
<string>List</string>
<key>Types</key>
<array>
<string>com.apple.cocoa.string</string>
</array>
</dict>
<key>ActionBundlePath</key>
<string>/System/Library/Automator/Run Shell Script.action</string>
<key>ActionName</key>
<string>Run Shell Script</string>
<key>ActionParameters</key>
<dict>
<key>COMMAND_STRING</key>
<string>#!/bin/bash
"\$HOME/.localcrypt/${script}"</string>
<key>CheckedForUserDefaultShell</key>
<true/>
<key>inputMethod</key>
<integer>0</integer>
<key>shell</key>
<string>/bin/bash</string>
<key>source</key>
<string></string>
</dict>
<key>BundleIdentifier</key>
<string>com.apple.RunShellScript</string>
<key>CFBundleVersion</key>
<string>2.0.3</string>
<key>CanShowSelectedItemsWhenRun</key>
<false/>
<key>CanShowWhenRun</key>
<true/>
<key>Category</key>
<array>
<string>AMCategoryUtilities</string>
</array>
<key>Class Name</key>
<string>RunShellScriptAction</string>
<key>InputUUID</key>
<string>${uuid_prefix}0</string>
<key>Keywords</key>
<array>
<string>Shell</string>
<string>Script</string>
<string>Command</string>
<string>Run</string>
<string>Unix</string>
</array>
<key>OutputUUID</key>
<string>${uuid_prefix}1</string>
<key>UUID</key>
<string>${uuid_prefix}2</string>
<key>UnlocalizedApplications</key>
<array>
<string>Automator</string>
</array>
<key>arguments</key>
<dict>
<key>0</key>
<dict>
<key>default value</key>
<integer>0</integer>
<key>name</key>
<string>inputMethod</string>
<key>required</key>
<string>0</string>
<key>type</key>
<string>0</string>
<key>uuid</key>
<string>0</string>
</dict>
<key>1</key>
<dict>
<key>default value</key>
<string></string>
<key>name</key>
<string>source</string>
<key>required</key>
<string>0</string>
<key>type</key>
<string>0</string>
<key>uuid</key>
<string>1</string>
</dict>
</dict>
<key>isViewVisible</key>
<true/>
<key>location</key>
<string>309.000000:253.000000</string>
<key>nibPath</key>
<string>/System/Library/Automator/Run Shell Script.action/Contents/Resources/Base.lproj/main.nib</string>
</dict>
<key>isViewVisible</key>
<true/>
</dict>
</array>
<key>connectors</key>
<dict/>
<key>workflowMetaData</key>
<dict>
<key>serviceInputTypeIdentifier</key>
<string>com.apple.Automator.text</string>
<key>serviceOutputTypeIdentifier</key>
<string>com.apple.Automator.text</string>
<key>serviceProcessesInput</key>
<integer>0</integer>
<key>workflowTypeIdentifier</key>
<string>com.apple.Automator.servicesMenu</string>
</dict>
</dict>
</plist>
WFLOW
# Info.plist — registers this workflow as a macOS Service
cat > "$dir/Info.plist" << INFOPLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSServices</key>
<array>
<dict>
<key>NSMenuItem</key>
<dict>
<key>default</key>
<string>${name}</string>
</dict>
<key>NSMessage</key>
<string>runWorkflowAsService</string>
<key>NSPortName</key>
<string>${name}</string>
<key>NSRequiredContext</key>
<dict/>
<key>NSSendTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
<key>NSReturnTypes</key>
<array>
<string>NSStringPboardType</string>
</array>
</dict>
</array>
</dict>
</plist>
INFOPLIST
}
write_workflow "Encrypt" "encrypt.sh" "A1B2C3D4-E5F6-7890-ABCD-EF123456789"
write_workflow "Decrypt" "decrypt.sh" "B1B2C3D4-E5F6-7890-ABCD-EF123456789"
ok "Workflows installed to $SERVICES_DIR"
# ── Register keyboard shortcuts ───────────────────────────────────────────────
# pbs (pasteboard server) manages Services registration.
# Modifier notation: @ = Cmd, ^ = Ctrl, $ = Shift
# So "@^$e" = Cmd+Ctrl+Shift+E
defaults write pbs NSServicesStatus \
'{"(null) - Encrypt - runWorkflowAsService" = {key_equivalent = "@^\$e"; };
"(null) - Decrypt - runWorkflowAsService" = {key_equivalent = "@^\$d"; };}'
# Flush the Services cache so macOS picks up the new workflows immediately
/System/Library/CoreServices/pbs -flush
killall WorkflowServiceRunner 2>/dev/null || true
ok "Shortcuts registered and Services cache flushed"
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
echo " ════════════════════════════════════════════"
echo " Installation complete!"
echo ""
echo " Encrypt: Cmd+Ctrl+Shift+E"
echo " Decrypt: Cmd+Ctrl+Shift+D"
echo ""
echo " HOW TO USE"
echo " 1. Select any text"
echo " 2. Press Cmd+Ctrl+Shift+E — text disappears"
echo " 3. Press Cmd+V to paste the ciphertext"
echo " 4. To decrypt: select ciphertext, press Cmd+Ctrl+Shift+D"
echo " — plaintext replaces it in-place"
echo ""
echo " Works best in Chrome and Chrome-based browsers."
echo " Also available via right-click → Services menu."
echo ""
echo " If shortcuts don't fire after install:"
echo " System Settings → Keyboard → Keyboard Shortcuts"
echo " → Services → Text → enable Encrypt + Decrypt"
echo " ════════════════════════════════════════════"
echo ""
# Offer to open System Settings for manual shortcut verification
read -rp " Open System Settings → Keyboard Shortcuts now? [y/N] " ans
if [[ "${ans,,}" == "y" ]]; then
open "x-apple.systempreferences:com.apple.preference.keyboard?Shortcuts" 2>/dev/null || true
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment