Skip to content

Instantly share code, notes, and snippets.

@ariel-frischer
Created December 12, 2025 20:43
Show Gist options
  • Select an option

  • Save ariel-frischer/dbd5da3bce0cc73762f13f08f36f05f2 to your computer and use it in GitHub Desktop.

Select an option

Save ariel-frischer/dbd5da3bce0cc73762f13f08f36f05f2 to your computer and use it in GitHub Desktop.
Cliphist secret cleaner - auto-remove passwords from Wayland clipboard history
#!/bin/bash
# =============================================================================
# CLIPHIST SECRET CLEANER
# Auto-removes passwords/secrets from clipboard history after a configurable delay
# =============================================================================
#
# DESCRIPTION:
# Monitors cliphist for high-entropy strings (likely passwords) and
# automatically deletes them after a delay. Uses Shannon entropy and
# character diversity to detect secrets.
#
# REQUIREMENTS:
# - cliphist (clipboard history manager for Wayland)
# - python3 (for entropy calculation)
# - awk (standard on most systems)
# - wl-paste watching cliphist:
# wl-paste --type text --watch cliphist store
# wl-paste --type image --watch cliphist store
#
# USAGE:
# cliphist-secret-cleaner [delay_seconds] [--dry-run]
#
# OPTIONS:
# delay_seconds Time to wait before deleting (default: 90)
# --dry-run Log detections but don't actually delete
#
# INSTALLATION:
# 1. Copy this script to ~/.local/bin/ and make executable:
# chmod +x ~/.local/bin/cliphist-secret-cleaner
#
# 2. Create systemd user service (~/.config/systemd/user/cliphist-secret-cleaner.service):
# [Unit]
# Description=Cliphist secret/password cleaner
# After=graphical-session.target
#
# [Service]
# Type=simple
# ExecStart=%h/.local/bin/cliphist-secret-cleaner 90
# Restart=on-failure
# RestartSec=5
#
# [Install]
# WantedBy=default.target
#
# 3. Enable and start the service:
# systemctl --user daemon-reload
# systemctl --user enable --now cliphist-secret-cleaner.service
#
# COMMANDS:
# View logs: journalctl --user -u cliphist-secret-cleaner -f
# Restart: systemctl --user restart cliphist-secret-cleaner
# Stop: systemctl --user stop cliphist-secret-cleaner
# Disable: systemctl --user disable --now cliphist-secret-cleaner
#
# DETECTION LOGIC:
# Flags as password if:
# - Length 8-128 chars, no spaces/newlines
# - Not a URL or file path
# - Either: entropy > 4.0 OR (entropy > 3.2 AND 3+ char classes)
# Character classes: uppercase, lowercase, digits, special chars
#
# =============================================================================
DELAY_SECONDS=${1:-90}
DRY_RUN=false
[[ "$*" == *"--dry-run"* ]] && DRY_RUN=true
CHECK_INTERVAL=10
declare -A pending_deletions
declare -A pending_info
log() {
echo "[$(date '+%H:%M:%S')] $1"
}
# Mask a string for safe logging (show first 3 and last 2 chars)
mask_string() {
local str="$1"
local len=${#str}
if [[ $len -le 8 ]]; then
echo "${str:0:2}$( printf '*%.0s' $(seq 1 $((len-2))) )"
else
echo "${str:0:3}***${str: -2}"
fi
}
# Calculate Shannon entropy of a string
calc_entropy() {
local str="$1"
echo "$str" | python3 -c "
import sys, math
from collections import Counter
s = sys.stdin.read().strip()
if not s: print(0); sys.exit()
freq = Counter(s)
length = len(s)
entropy = -sum((c/length) * math.log2(c/length) for c in freq.values())
print(f'{entropy:.2f}')
"
}
# Check if string looks like a password/secret, returns info string if true
check_secret() {
local str="$1"
local len=${#str}
# Skip if too short, too long, or has newlines
[[ $len -lt 8 || $len -gt 128 ]] && return 1
[[ "$str" == *$'\n'* ]] && return 1
# Skip if contains spaces (likely normal text)
[[ "$str" == *" "* ]] && return 1
# Skip URLs
[[ "$str" =~ ^https?:// ]] && return 1
# Skip file paths
[[ "$str" =~ ^[/~] ]] && return 1
# Skip if it's just numbers
[[ "$str" =~ ^[0-9]+$ ]] && return 1
# Calculate entropy
local entropy=$(calc_entropy "$str")
# Check character class diversity
local has_upper=0 has_lower=0 has_digit=0 has_special=0
[[ "$str" =~ [A-Z] ]] && has_upper=1
[[ "$str" =~ [a-z] ]] && has_lower=1
[[ "$str" =~ [0-9] ]] && has_digit=1
[[ "$str" =~ [^a-zA-Z0-9] ]] && has_special=1
local diversity=$((has_upper + has_lower + has_digit + has_special))
local reason=""
# Likely a password if: high entropy OR (moderate entropy + diverse chars)
if awk "BEGIN {exit !($entropy > 4.0)}"; then
reason="high entropy ($entropy)"
elif awk "BEGIN {exit !($entropy > 3.2)}" && [[ $diversity -ge 3 ]]; then
reason="entropy=$entropy, diversity=$diversity/4"
else
return 1
fi
# Return info about why it was flagged
echo "len=$len, $reason"
return 0
}
mode_str="LIVE"
$DRY_RUN && mode_str="DRY-RUN"
log "Starting cliphist secret cleaner [$mode_str] (delay: ${DELAY_SECONDS}s)"
$DRY_RUN && log "Dry-run mode: will log but NOT delete. Remove --dry-run to enable deletion."
while true; do
current_time=$(date +%s)
# Process pending deletions
for entry_id in "${!pending_deletions[@]}"; do
delete_time=${pending_deletions[$entry_id]}
if [[ $current_time -ge $delete_time ]]; then
info="${pending_info[$entry_id]}"
if cliphist list | grep -q "^${entry_id} "; then
if $DRY_RUN; then
log "WOULD DELETE #$entry_id - $info"
else
echo "$entry_id" | cliphist delete
log "DELETED #$entry_id - $info"
fi
fi
unset "pending_deletions[$entry_id]"
unset "pending_info[$entry_id]"
fi
done
# Check recent entries for secrets (only check newest few)
while IFS= read -r line; do
entry_id=$(echo "$line" | cut -f1)
content=$(echo "$line" | cut -f2-)
# Skip if already pending
[[ -n "${pending_deletions[$entry_id]}" ]] && continue
reason=$(check_secret "$content")
if [[ $? -eq 0 ]]; then
delete_at=$((current_time + DELAY_SECONDS))
masked=$(mask_string "$content")
pending_deletions[$entry_id]=$delete_at
pending_info[$entry_id]="'$masked' ($reason)"
log "QUEUED #$entry_id for deletion in ${DELAY_SECONDS}s: '$masked' ($reason)"
fi
done < <(cliphist list | head -5)
sleep $CHECK_INTERVAL
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment