From 000a41286f6b6cea31b26cb01e122075af2b107a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Pe=C5=A1ek=20=28Kof=29?= Date: Sat, 7 Nov 2026 11:50:31 +0100 Subject: [PATCH] feat: add link-sanitizer to strip tracking params & parasitic telemetry --- link-sanitizer | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100755 link-sanitizer diff --git a/link-sanitizer b/link-sanitizer new file mode 100755 index 0000000..1c1ae64 --- /dev/null +++ b/link-sanitizer @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# link-sanitizer — Purges surveillance tracking tokens & parasitic telemetry from URLs +# Part of the Techné zero-bloat toolkit for the small-web & dissident networks. +# CC0 1.0 Universal / Public Domain. Author: --kof + +set -euo pipefail + +clean_url() { + local raw_url="$1" + + # Strip invisible zero-width spaces (ZWSP, ZWNJ, ZWJ, BOM) & non-breaking spaces + local url + url=$(printf "%s" "$raw_url" | sed 's/\xc2\xa0/ /g; s/\xe2\x80\x8b//g; s/\xe2\x80\x8c//g; s/\xe2\x80\x8d//g; s/\xef\xbb\xbf//g; s/^[[:space:]]*//; s/[[:space:]]*$//') + + if [[ -z "$url" ]]; then + return + fi + + # Python one-liner to parse query parameters and strip tracking tokens + python3 -c " +import sys +from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse + +TRACKING_KEYS = { + # UTM & Campaign parameters + 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', + # Meta / Facebook / Instagram + 'fbclid', 'fref', 'igshid', + # Google / Alphabet + 'gclid', 'gclsrc', 'dclid', 'gbraid', 'wbraid', + # YouTube tracking + 'si', 'feature', 'pp', + # Twitter / X + 'ref_src', 'ref_url', + # Newsletters / CRM + 'mc_cid', 'mc_eid', 'vero_id', 'vero_conv', 'wickedid', + # TikTok & Bytedance + '_r', '_t', 'share_app_name', + # Misc telemetry & affiliate bloat + 'msclkid', 'yclid', 'rb_clickid', 's_kwcid', '_openstat', 'action_object_map', + 'action_type_map', 'action_ref_map' +} + +for line in sys.stdin: + raw = line.strip() + if not raw: + continue + # If raw doesn't have scheme but looks like url, handle gracefully + parsed = urlparse(raw) + if not parsed.netloc and not raw.startswith('http'): + print(raw) + continue + + # Filter query parameters + query_pairs = parse_qsl(parsed.query, keep_blank_values=True) + filtered = [(k, v) for k, v in query_pairs if k.lower() not in TRACKING_KEYS] + + # Clean query string + new_query = urlencode(filtered) + cleaned = urlunparse(( + parsed.scheme, + parsed.netloc, + parsed.path, + parsed.params, + new_query, + parsed.fragment + )) + # Strip trailing ? if query is now empty + if cleaned.endswith('?'): + cleaned = cleaned[:-1] + print(cleaned) +" <<< "$url" +} + +if [[ "${1:-}" == "-c" || "${1:-}" == "--clipboard" ]]; then + if command -v xclip >/dev/null 2>&1; then + clip=$(xclip -selection clipboard -o 2>/dev/null || true) + if [[ -n "$clip" ]]; then + cleaned=$(clean_url "$clip") + printf "%s" "$cleaned" | xclip -selection clipboard -i + printf "Sanitized clipboard: %s\n" "$cleaned" + fi + else + echo "Error: xclip not found" >&2 + exit 1 + fi +elif [[ $# -gt 0 ]]; then + for arg in "$@"; do + clean_url "$arg" + done +else + # Process from stdin line by line + while IFS= read -r line || [[ -n "$line" ]]; do + clean_url "$line" + done +fi