Created
April 11, 2026 11:53
-
-
Save guessi/5969ebabf138b144dd38985db7bb465a to your computer and use it in GitHub Desktop.
Strip all sensitive metadata from image files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # strip-metadata.sh — Strip all sensitive metadata from image files. | |
| # | |
| # Images can contain hidden metadata that leaks private information: | |
| # - EXIF: camera model, serial number, software version, timestamps | |
| # - GPS: exact latitude/longitude where the photo was taken | |
| # - ICC profiles: color profiles that can reveal the device or software used | |
| # - XMP/IPTC: author names, copyright info, editing history | |
| # | |
| # This script uses exiftool to remove ALL of the above in one pass. | |
| # | |
| # Usage: | |
| # ./strip-metadata.sh [options] <image> [image2 ...] | |
| # | |
| # Options: | |
| # -n, --dry-run Show metadata without removing | |
| # -y, --yes Skip confirmation prompt | |
| # -h, --help Show this help message | |
| # | |
| # Examples: | |
| # ./strip-metadata.sh photo.jpg Single file | |
| # ./strip-metadata.sh *.jpg *.png Batch process | |
| # ./strip-metadata.sh -n photo.jpg Preview what would be removed | |
| # ./strip-metadata.sh -y *.jpg Strip without confirmation | |
| set -o errexit | |
| set -o nounset | |
| set -o pipefail | |
| usage() { | |
| sed -n '/^# Usage:/,/^[^#]/{/^#/s/^# \{0,1\}//p;}' "$0" | |
| exit "${1:-0}" | |
| } | |
| # Require exiftool — the standard tool for reading/writing image metadata | |
| if ! command -v exiftool &>/dev/null; then | |
| echo "Error: exiftool is required." >&2 | |
| echo " macOS: brew install exiftool" >&2 | |
| echo " Debian: apt install libimage-exiftool-perl" >&2 | |
| echo " Fedora: dnf install perl-Image-ExifTool" >&2 | |
| exit 1 | |
| fi | |
| echo "Using: $(command -v exiftool)" | |
| echo | |
| DRY_RUN=false | |
| SKIP_CONFIRM=false | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -n|--dry-run) DRY_RUN=true; shift ;; | |
| -y|--yes) SKIP_CONFIRM=true; shift ;; | |
| -h|--help) usage 0 ;; | |
| --) shift; break ;; | |
| -*) echo "Unknown option: $1" >&2; usage 1 ;; | |
| *) break ;; | |
| esac | |
| done | |
| if [[ $# -eq 0 ]]; then | |
| echo "Error: no files specified." >&2 | |
| usage 1 | |
| fi | |
| # Warn before destructive operation | |
| if ! $DRY_RUN && ! $SKIP_CONFIRM; then | |
| echo "This will permanently strip metadata from $# file(s) in place." | |
| read -rp "Continue? [y/N] " answer | |
| [[ "$answer" =~ ^[Yy]$ ]] || { echo "Aborted."; exit 0; } | |
| fi | |
| processed=0 | |
| skipped=0 | |
| for file in "$@"; do | |
| if [[ -L "$file" ]]; then | |
| echo "Skipping: $file (symlink)" >&2 | |
| skipped=$((skipped + 1)) | |
| continue | |
| fi | |
| if [[ ! -f "$file" ]]; then | |
| echo "Skipping: $file (not found)" >&2 | |
| skipped=$((skipped + 1)) | |
| continue | |
| fi | |
| mime=$(file --brief --mime-type "$file") | |
| if [[ "$mime" != image/* ]]; then | |
| echo "Skipping: $file (not an image: $mime)" >&2 | |
| skipped=$((skipped + 1)) | |
| continue | |
| fi | |
| echo "=== $file ===" | |
| if $DRY_RUN; then | |
| # -G1: show metadata group names (e.g. [EXIF], [GPS], [ICC_Profile]) | |
| # so you can see exactly where each tag comes from | |
| exiftool -G1 "$file" | |
| echo | |
| processed=$((processed + 1)) | |
| continue | |
| fi | |
| # -all= Remove all metadata tags (EXIF, XMP, IPTC, GPS, etc.) | |
| # -icc_profile:all= Also remove ICC color profiles (not covered by -all=) | |
| # -overwrite_original Modify the file in place without creating a backup | |
| if ! exiftool -all= -icc_profile:all= -overwrite_original "$file"; then | |
| echo "Warning: could not strip metadata from $file" >&2 | |
| skipped=$((skipped + 1)) | |
| echo | |
| continue | |
| fi | |
| # Verify: count remaining tags, excluding ExifTool/System/File groups | |
| # which are structural (image dimensions, encoding) and can't be removed | |
| remaining=$(exiftool -G1 -s "$file" | grep -cv '^\[ExifTool\]\|^\[System\]\|^\[File\]' || true) | |
| echo "Done. Remaining non-structural tags: $remaining" | |
| echo | |
| processed=$((processed + 1)) | |
| done | |
| echo "---" | |
| if $DRY_RUN; then | |
| echo "Previewed: $processed | Skipped: $skipped" | |
| else | |
| echo "Stripped: $processed | Skipped: $skipped" | |
| fi | |
| if [[ $processed -eq 0 ]]; then | |
| exit 1 | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment