Skip to content

Instantly share code, notes, and snippets.

@jftuga
Created July 19, 2026 11:17
Show Gist options
  • Select an option

  • Save jftuga/f01d46236d9a1f184b68a1eb947b15be to your computer and use it in GitHub Desktop.

Select an option

Save jftuga/f01d46236d9a1f184b68a1eb947b15be to your computer and use it in GitHub Desktop.
flock: Toggle file protection on macOS.
#!/bin/zsh
set -uo pipefail
# flock: Toggle file protection on macOS.
#
# Locks files and directories by stashing the original permissions in an
# xattr (com.flock.mode), stripping write bits, changing ownership to
# root:wheel, and setting the system immutable flag (schg). This prevents
# modification even by the file owner unless the lock is explicitly removed
# with sudo.
#
# Unlocks files and directories by removing the immutable flag, restoring
# ownership to the calling user, and restoring the original permissions
# from the saved xattr. Falls back to 644/755 if no xattr is present.
#
# Usage:
# flock ro [-f] file1 file2 ... # lock (read-only; -f = files only)
# flock rw [-f] file1 file2 ... # unlock (read-write; -f = files only)
# flock ls [file ...] # show protection state (defaults to cwd)
# flock lsl [file ...] # list only locked files
# flock lsu [file ...] # list only unlocked files
# funlock file1 file2 ... # shortcut: equivalent to "flock rw", typically sym-linked
#
# Requires: sudo privileges (ro/rw only).
readonly PGM_VERSION="1.0.0"
readonly CALLER_USER="$USER"
readonly CALLER_GROUP="staff"
readonly XATTR_MODE="com.flock.mode"
# Print usage message and exit.
usage() {
echo "Usage: flock <ro|rw|ls|lsl|lsu> [-f] <file> [file ...]" >&2
echo " -f files only (skip directories); valid with ro/rw" >&2
exit 0
}
# Lock a file or directory by stashing the current mode in an xattr,
# stripping write bits, setting root ownership, and applying the system
# immutable flag (schg).
lock_file() {
local file="$1"
local orig_mode locked_mode
orig_mode="$(stat -f '%Lp' "$file")"
locked_mode="$(printf '%o' $(( 8#${orig_mode} & ~8#0222 )))"
echo "Locking: $file (mode $orig_mode -> $locked_mode)"
sudo xattr -w "$XATTR_MODE" "$orig_mode" "$file"
sudo chown root:wheel "$file"
sudo chmod "$locked_mode" "$file"
sudo chflags schg "$file"
}
# Unlock a file or directory by removing the immutable flag, restoring
# ownership to the calling user, and restoring the original mode from
# the saved xattr. Falls back to 644/755 if no xattr is present.
unlock_file() {
local file="$1"
local orig_mode
echo "Unlocking: $file"
sudo chflags noschg "$file"
sudo chown "${CALLER_USER}:${CALLER_GROUP}" "$file"
if orig_mode="$(sudo xattr -p "$XATTR_MODE" "$file" 2>/dev/null)"; then
sudo chmod "$orig_mode" "$file"
xattr -d "$XATTR_MODE" "$file"
else
if [[ -d "$file" ]]; then
sudo chmod 755 "$file"
else
sudo chmod 644 "$file"
fi
fi
}
# Return 0 if a file should be shown for the given verb, 1 otherwise.
should_show() {
local file="$1"
local verb="$2"
if [[ "$verb" == "ls" ]]; then
return 0
fi
local flags
flags="$(stat -L -f '%Sf' "$file")"
if [[ "$verb" == "lsl" && "$flags" == *schg* ]]; then
return 0
elif [[ "$verb" == "lsu" && "$flags" != *schg* ]]; then
return 0
fi
return 1
}
# Display the lock state, ownership, mode, and flags of a file or directory.
# Shows the saved original mode if present. Output is padded by filename
# length for aligned columnar display.
show_status() {
local file="$1"
local pad="$2"
local flags owner perms state saved_mode
flags="$(stat -L -f '%Sf' "$file")"
if [[ "$flags" == *schg* ]]; then
state=" LOCKED"
else
state="unlocked"
fi
owner="$(stat -L -f '%Su:%Sg' "$file")"
perms="$(stat -L -f '%Sp' "$file")"
saved_mode="$(xattr -p "$XATTR_MODE" "$file" 2>/dev/null)" || true
if [[ -n "$saved_mode" ]]; then
printf "%-${pad}s %s owner=%s mode=%s saved=%s flags=%s\n" \
"$file" "$state" "$owner" "$perms" "$saved_mode" "${flags:-(none)}"
else
printf "%-${pad}s %s owner=%s mode=%s flags=%s\n" \
"$file" "$state" "$owner" "$perms" "${flags:-(none)}"
fi
}
# Parse arguments, validate the verb, and dispatch to the appropriate function.
if [[ $# -lt 1 ]]; then
usage
fi
echo $0
if [[ "${0:t}" == "funlock" ]]; then
verb="rw"
else
verb="$1"
shift
fi
if [[ "$verb" != "ro" && "$verb" != "rw" && "$verb" != "ls" && "$verb" != "lsl" && "$verb" != "lsu" ]]; then
echo "Error: verb must be 'ro', 'rw', 'ls', 'lsl, or 'lsu', got '$verb'" >&2
usage
fi
files_only=false
if [[ "$verb" == "ro" || "$verb" == "rw" ]] && [[ ${1:-} == "-f" ]]; then
files_only=true
shift
fi
if [[ $# -eq 0 ]]; then
if [[ "$verb" == ls* ]]; then
set -- *(D)
else
usage
fi
fi
# Find the longest filename for columnar padding, considering only files
# that will actually be displayed for the current verb.
max_len=0
for file in "$@"; do
if [[ ! -e "$file" ]]; then
continue
fi
if $files_only && [[ -d "$file" ]]; then
continue
fi
if [[ "$verb" == ls* ]] && should_show "$file" "$verb" && (( ${#file} > max_len )); then
max_len=${#file}
fi
done
# Process each target, skipping any that do not exist.
for file in "$@"; do
if [[ ! -e "$file" ]]; then
echo "Error: '$file' does not exist, skipping." >&2
continue
fi
if $files_only && [[ -d "$file" ]]; then
continue
fi
if [[ "$verb" == ls* ]]; then
should_show "$file" "$verb" && show_status "$file" "$max_len"
elif [[ "$verb" == "ro" ]]; then
lock_file "$file"
else
unlock_file "$file"
fi
done
exit 0
@jftuga

jftuga commented Jul 19, 2026

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment