Skip to content

Instantly share code, notes, and snippets.

@aaronedev
Created August 11, 2025 09:46
Show Gist options
  • Save aaronedev/f200eb20b072d13b2683d7503b94620b to your computer and use it in GitHub Desktop.
Save aaronedev/f200eb20b072d13b2683d7503b94620b to your computer and use it in GitHub Desktop.
bash script to fix permissions in a user's home folder
##############################################################################
# fixuserhome.sh
# Script to fix permissions in a user's home folder
# Author: https://github.com/aaronedev
# Version: 1.0
# License: MIT
# Date: 2025-08-11
##############################################################################
# Usage: fixuserhome [OPTIONS] [username] [exclude_dir1] [exclude_dir2] ...
# Example: fixuserhome -t testuser /var/log /tmp
# Options:
# -h, --help Show this help message
# -t, --test Test mode: run on a test directory instead of home
# -d, --dry-run Dry run: show what would be done without making changes
# -u, --user Target user (default: current user)
##############################################################################
function fixuserhome() {
# Check for help flag
if [[ "${1}" == "-h" ]] || [[ "${1}" == "--help" ]] || [[ "${1}" == "-?" ]]; then
echo -e "${BRIGHT_CYAN}Usage:${RESET} fixuserhome [OPTIONS] [username] [exclude_dir1] [exclude_dir2] ..."
echo
echo -e "Fix permissions in a user's home folder with optional directory exclusions."
echo
echo -e "${BRIGHT_YELLOW}Options:${RESET}"
echo -e " ${BRIGHT_GREEN}-h, --help${RESET} Show this help message"
echo -e " ${BRIGHT_GREEN}-t, --test${RESET} Test mode: run on a test directory instead of home"
echo -e " ${BRIGHT_GREEN}-d, --dry-run${RESET} Dry run: show what would be done without making changes"
echo
echo -e "${BRIGHT_YELLOW}Arguments:${RESET}"
echo -e " ${BRIGHT_GREEN}username${RESET} Target user (default: current user)"
echo -e " ${BRIGHT_GREEN}exclude_dir${RESET}... Directories to skip (relative or absolute paths)"
echo
echo -e "${BRIGHT_YELLOW}Examples:${RESET}"
echo -e " ${DIM}# Fix current user's home${RESET}"
echo -e " fixuserhome"
echo
echo -e " ${DIM}# Fix john's home${RESET}"
echo -e " fixuserhome john"
echo
echo -e " ${DIM}# Fix john's home, skip Downloads and .cache${RESET}"
echo -e " fixuserhome john Downloads .cache"
echo
echo -e " ${DIM}# Fix current user's home, skip Downloads${RESET}"
echo -e " fixuserhome \"\" Downloads"
echo
echo -e " ${DIM}# Test on ./testdir instead of home${RESET}"
echo -e " fixuserhome -t testdir"
echo
echo -e " ${DIM}# Dry run for john's home${RESET}"
echo -e " fixuserhome -d john"
echo
echo -e "${BRIGHT_RED}Note:${RESET} Sensitive directories (.ssh, .gnupg, etc.) are always processed for security."
return 0
fi
local _test_mode=false
local _dry_run=false
local _test_dir=""
# Statistics tracking
local _stats_files_processed=0
local _stats_dirs_processed=0
local _stats_sensitive_dirs=()
local _stats_scripts_made_exec=0
local _stats_excluded_dirs=0
# Parse options
while [[ "${1}" =~ ^- ]]; do
case "${1}" in
-t | --test)
_test_mode=true
shift
if [[ -z "${1}" ]] || [[ "${1}" =~ ^- ]]; then
echo -e "${BRIGHT_RED}Error:${RESET} Test mode requires a directory path"
return 1
fi
_test_dir="${1}"
shift
;;
-d | --dry-run)
_dry_run=true
shift
;;
*)
echo -e "${BRIGHT_RED}Error:${RESET} Unknown option: ${1}"
return 1
;;
esac
done
local _username
if [[ "${_test_mode}" == true ]]; then
_username="$(whoami)"
elif [[ -z "${1}" ]]; then
_username="$(whoami)"
else
_username="${1}"
shift
fi
# Parse excluded directories from remaining arguments
local -a _exclude_dirs=("$@")
_stats_excluded_dirs=${#_exclude_dirs[@]}
local _home_dir
if [[ "${_test_mode}" == true ]]; then
if [[ ! -d "${_test_dir}" ]]; then
echo -e "${BRIGHT_RED}Error:${RESET} Test directory ${BRIGHT_CYAN}${_test_dir}${RESET} does not exist"
return 1
fi
_home_dir="$(realpath "${_test_dir}")"
echo -e "${BRIGHT_YELLOW}TEST MODE:${RESET} Operating on ${BRIGHT_CYAN}${_home_dir}${RESET}"
else
_home_dir=$(getent passwd "${_username}" 2>/dev/null | cut -d: -f6)
if [[ -z "${_home_dir}" ]] || [[ ! -d "${_home_dir}" ]]; then
_home_dir="/home/${_username}"
fi
fi
if [[ "${_dry_run}" == true ]]; then
echo -e "${BRIGHT_YELLOW}DRY RUN MODE:${RESET} No changes will be made"
fi
if [ -d "${_home_dir}" ]; then
echo -e "Home directory found: ${BRIGHT_YELLOW}${_home_dir}${RESET}"
else
echo -e "${BRIGHT_RED}Error:${RESET} User ${BRIGHT_CYAN}${_username}${RESET} does not have a home directory"
return 1
fi
if ! sudo -v && [[ "${_dry_run}" != true ]]; then
echo -e "${BRIGHT_RED}Error:${RESET} You do not have sufficient permissions to run this script with necessary privileges"
return 1
fi
# Count total files and directories before processing
if [[ "${_dry_run}" != true ]]; then
echo -e "${DIM}Counting files and directories...${RESET}"
if [[ ${#_exclude_dirs[@]} -gt 0 ]]; then
local _find_count_excludes=""
for dir in "${_exclude_dirs[@]}"; do
local _abs_dir
if [[ "${dir}" = /* ]]; then
_abs_dir="${dir}"
else
_abs_dir="${_home_dir}/${dir#./}"
fi
_find_count_excludes="${_find_count_excludes} -path '${_abs_dir}' -prune -o"
done
_stats_files_processed=$(find ${_home_dir} ${_find_count_excludes} -type f -print 2>/dev/null | wc -l)
_stats_dirs_processed=$(find ${_home_dir} ${_find_count_excludes} -type d -print 2>/dev/null | wc -l)
else
_stats_files_processed=$(find ${_home_dir} -type f 2>/dev/null | wc -l)
_stats_dirs_processed=$(find ${_home_dir} -type d 2>/dev/null | wc -l)
fi
fi
# Wrapper for commands that respects dry-run mode
function _run() {
local _description="${1}"
local _command="${2}"
if [[ "${_dry_run}" == true ]]; then
echo -e "${BRIGHT_CYAN}[DRY-RUN]${RESET} ${_description}: ${_command}"
else
runwithfeedback "${_description}" "${_command}"
fi
}
# Build find exclusion parameters
local _find_excludes=""
local _find_prune=""
if [[ ${#_exclude_dirs[@]} -gt 0 ]]; then
echo -e "${BRIGHT_YELLOW}Excluding directories:${RESET}"
for dir in "${_exclude_dirs[@]}"; do
local _abs_dir
if [[ "${dir}" = /* ]]; then
_abs_dir="${dir}"
else
_abs_dir="${_home_dir}/${dir#./}"
fi
echo -e " - ${_abs_dir}"
_find_prune="${_find_prune} -path '${_abs_dir}' -prune -o"
done
_find_excludes="${_find_prune}"
fi
if ask "${BRIGHT_RED}WARNING:${RESET} Change all permissions for user ${BRIGHT_CYAN}${_username}${RESET}'s home folder?" N; then
if ask "Reset group ownership permissions to ${BRIGHT_CYAN}${_username}${RESET}?" Y; then
if [[ -n "${_find_excludes}" ]]; then
_run \
"Set the owner and group as ${_username} (excluding specified directories)" \
"find ${_home_dir} ${_find_excludes} -print0 | xargs -0 sudo chown -h ${_username}:${_username}"
else
_run \
"Set the owner and group as ${_username}" \
"sudo chown -Rh ${_username}:${_username} ${_home_dir}"
fi
else
if [[ -n "${_find_excludes}" ]]; then
_run \
"Set the owner as ${_username} (excluding specified directories)" \
"find ${_home_dir} ${_find_excludes} -print0 | xargs -0 sudo chown -h ${_username}"
else
_run \
"Set the owner as ${_username}" \
"sudo chown -Rh ${_username} ${_home_dir}"
fi
fi
if [[ "${_dry_run}" != true ]]; then
fixinvalidexecutepermissions "${_home_dir}"
fi
if [[ -n "${_find_excludes}" ]]; then
_run \
"Make sure we have read and write access (excluding specified directories)" \
"find ${_home_dir} ${_find_excludes} -print0 | xargs -0 chmod u+rw"
_run \
"Remove write access from group (excluding specified directories)" \
"find ${_home_dir} ${_find_excludes} -print0 | xargs -0 chmod g-w"
_run \
"Remove all access from others (excluding specified directories)" \
"find ${_home_dir} ${_find_excludes} -print0 | xargs -0 chmod o-rwx"
# Count .sh files
if [[ "${_dry_run}" != true ]]; then
_stats_scripts_made_exec=$(find ${_home_dir} ${_find_excludes} -type f \( -name "*.sh" -o -name ".*.sh" \) -print 2>/dev/null | wc -l)
fi
_run \
"Make .sh shell script files executable (excluding specified directories)" \
"find ${_home_dir} ${_find_excludes} -type f \( -name \"*.sh\" -o -name \".*.sh\" \) -print0 | xargs -0 chmod ug+x"
_run \
"Make sure all directories have execute permissions (excluding specified directories)" \
"find ${_home_dir} ${_find_excludes} -type d -print0 | xargs -0 chmod ug+X"
_run \
"Remove group permissions for directories without group read (excluding specified directories)" \
"find ${_home_dir} ${_find_excludes} -type d ! -perm -g+r -print0 | xargs -0 chmod g-wx"
else
_run \
"Make sure we have read and write access" \
"chmod -R u+rw ${_home_dir}"
_run \
"Remove write access from group" \
"chmod -R g-w ${_home_dir}"
_run \
"Remove all access from others" \
"chmod -R o-rwx ${_home_dir}"
# Count .sh files
if [[ "${_dry_run}" != true ]]; then
_stats_scripts_made_exec=$(find ${_home_dir} -type f \( -name "*.sh" -o -name ".*.sh" \) 2>/dev/null | wc -l)
fi
_run \
"Make .sh shell script files executable" \
"find ${_home_dir} -type f \( -name \"*.sh\" -o -name \".*.sh\" \) -exec chmod ug+x {} \;"
_run \
"Make sure all directories have execute permissions" \
"chmod -R ug+X ${_home_dir}"
_run \
"Remove group permissions for directories without group read" \
"find ${_home_dir} -type d ! -perm -g+r -execdir chmod g-wx {} \;"
fi
# Handle sensitive directories (track which ones exist)
if [[ -d "${_home_dir}/.local/share/kwalletd" ]]; then
_stats_sensitive_dirs+=("KDE Wallet")
_run \
"User only access to KDE Wallet keyring" \
"chmod -R go-rwx ${_home_dir}/.local/share/kwalletd"
fi
if [[ -d "${_home_dir}/.local/share/keyrings" ]]; then
_stats_sensitive_dirs+=("GNOME Keyring")
_run \
"User only access to GNOME keyring" \
"chmod -R go-rwx ${_home_dir}/.local/share/keyrings"
fi
if [[ -d "${_home_dir}/.ssh" ]]; then
_stats_sensitive_dirs+=("SSH")
_run \
"Setting ownership for .ssh directory and files" \
"chown -Rh ${_username}:${_username} ${_home_dir}/.ssh"
_run \
"User only access to .ssh and private keys" \
"chmod -R go-rwx ${_home_dir}/.ssh"
fi
if [[ -d "${_home_dir}/.putty" ]]; then
_stats_sensitive_dirs+=("PuTTY")
_run \
"User only access to .putty and ssh keys" \
"chmod -R go-rwx ${_home_dir}/.putty"
fi
if [[ -d "${_home_dir}/.pki" ]]; then
_stats_sensitive_dirs+=("PKI")
_run \
"User only access to .pki keys and certificates" \
"chmod -R go-rwx ${_home_dir}/.pki"
fi
if [[ -d "${_home_dir}/.gnupg" ]]; then
_stats_sensitive_dirs+=("GnuPG")
_run \
"User only access to .gnupg and private keys" \
"chmod -R go-rwx ${_home_dir}/.gnupg"
fi
if hascommand --strict keepassxc || hascommand --strict keepass || hascommand --strict keeweb; then
local _kdbx_count=$(find ${_home_dir} -type f \( -name '*.kdbx' -o -name '.*.kdbx' \) 2>/dev/null | wc -l)
if [[ ${_kdbx_count} -gt 0 ]]; then
_stats_sensitive_dirs+=("KeePass (${_kdbx_count} files)")
fi
_run \
"User only access to KeePassXC/KeePass/KeeWeb .kdbx files" \
"find ${_home_dir} -type f \( -name '*.kdbx' -o -name '.*.kdbx' \) -exec chmod go-rwx {} \;"
fi
if [[ -d "${_home_dir}/.password-store" ]]; then
_stats_sensitive_dirs+=("pass")
_run \
"User only access to pass data" \
"chmod -R go-rwx ${_home_dir}/.password-store"
fi
if [[ -d "${_home_dir}/.config/Bitwarden" ]]; then
_stats_sensitive_dirs+=("Bitwarden")
_run \
"User only access to Bitwarden data" \
"chmod -R go-rwx ${_home_dir}/.config/Bitwarden"
fi
if [[ -d "${_home_dir}/.var/app/com.bitwarden.desktop" ]]; then
_stats_sensitive_dirs+=("Bitwarden Flatpak")
_run \
"User only access to Bitwarden data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/com.bitwarden.desktop"
fi
if [[ -d "${_home_dir}/.local/share/torbrowser" ]]; then
_stats_sensitive_dirs+=("Tor Browser")
_run \
"User only access to Tor browser data" \
"chmod -R go-rwx ${_home_dir}/.local/share/torbrowser"
fi
if [[ -d "${_home_dir}/.var/app/com.github.micahflee.torbrowser-launcher" ]]; then
_stats_sensitive_dirs+=("Tor Browser Flatpak")
_run \
"User only access to Tor browser data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/com.github.micahflee.torbrowser-launcher"
fi
if [[ -d "${_home_dir}/.config/BraveSoftware" ]]; then
_stats_sensitive_dirs+=("Brave")
_run \
"User only access to Brave browser data" \
"chmod -R go-rwx ${_home_dir}/.config/BraveSoftware"
fi
if [[ -d "${_home_dir}/.var/app/com.brave.Browser" ]]; then
_stats_sensitive_dirs+=("Brave Flatpak")
_run \
"User only access to Brave browser data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/com.brave.Browser"
fi
if [[ -d "${_home_dir}/.config/google-chrome" ]]; then
_stats_sensitive_dirs+=("Chrome")
_run \
"User only access to Chrome browser data" \
"chmod -R go-rwx ${_home_dir}/.config/google-chrome"
fi
if [[ -d "${_home_dir}/.var/app/com.google.Chrome" ]]; then
_stats_sensitive_dirs+=("Chrome Flatpak")
_run \
"User only access to Chrome browser data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/com.google.Chrome"
fi
if [[ -d "${_home_dir}/.config/chromium" ]]; then
_stats_sensitive_dirs+=("Chromium")
_run \
"User only access to Chromium browser data" \
"chmod -R go-rwx ${_home_dir}/.config/chromium"
fi
if [[ -d "${_home_dir}/.var/app/org.chromium.Chromium" ]]; then
_stats_sensitive_dirs+=("Chromium Flatpak")
_run \
"User only access to Chromium browser data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/org.chromium.Chromium"
fi
if [[ -d "${_home_dir}/.var/app/net.sourceforge.chromium-bsu" ]]; then
_stats_sensitive_dirs+=("Ungoogled Chromium Flatpak")
_run \
"User only access to Ungoogled Chromium browser data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/net.sourceforge.chromium-bsu"
fi
if [[ -d "${_home_dir}/.mozilla" ]]; then
_stats_sensitive_dirs+=("Firefox")
_run \
"User only access to Firefox browser data" \
"chmod -R go-rwx ${_home_dir}/.mozilla"
fi
if [[ -d "${_home_dir}/.var/app/org.mozilla.firefox" ]]; then
_stats_sensitive_dirs+=("Firefox Flatpak")
_run \
"User only access to Firefox browser data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/org.mozilla.firefox"
fi
if [[ -d "${_home_dir}/.librewolf" ]]; then
_stats_sensitive_dirs+=("LibreWolf")
_run \
"User only access to LibreWolf browser data" \
"chmod -R go-rwx ${_home_dir}/.librewolf"
fi
if [[ -d "${_home_dir}/.var/app/io.gitlab.librewolf-community" ]]; then
_stats_sensitive_dirs+=("LibreWolf Flatpak")
_run \
"User only access to LibreWolf browser data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/io.gitlab.librewolf-community"
fi
if [[ -d "${_home_dir}/.config/opera" ]]; then
_stats_sensitive_dirs+=("Opera")
_run \
"User only access to Opera browser data" \
"chmod -R go-rwx ${_home_dir}/.config/opera"
fi
if [[ -d "${_home_dir}/.config/vivaldi" ]]; then
_stats_sensitive_dirs+=("Vivaldi")
_run \
"User only access to Vivaldi browser data" \
"chmod -R go-rwx ${_home_dir}/.config/vivaldi"
fi
if [[ -d "${_home_dir}/.config/microsoft-edge" ]]; then
_stats_sensitive_dirs+=("Edge")
_run \
"User only access to Microsoft Edge browser data" \
"chmod -R go-rwx ${_home_dir}/.config/microsoft-edge"
fi
if [[ -d "${_home_dir}/.config/microsoft-edge-beta" ]]; then
_stats_sensitive_dirs+=("Edge Beta")
_run \
"User only access to Microsoft Edge browser data" \
"chmod -R go-rwx ${_home_dir}/.config/microsoft-edge-beta"
fi
if [[ -d "${_home_dir}/.config/evolution" ]]; then
_stats_sensitive_dirs+=("Evolution")
_run \
"User only access to Evolution email data" \
"chmod -R go-rwx ${_home_dir}/.config/evolution"
fi
if [[ -d "${_home_dir}/.var/app/org.gnome.Evolution" ]]; then
_stats_sensitive_dirs+=("Evolution Flatpak")
_run \
"User only access to Evolution email data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/org.gnome.Evolution"
fi
if [[ -d "${_home_dir}/.local/share/geary" ]]; then
_stats_sensitive_dirs+=("Geary")
_run \
"User only access to Geary email data" \
"chmod -R go-rwx ${_home_dir}/.local/share/geary"
fi
if [[ -d "${_home_dir}/.var/app/org.gnome.Geary" ]]; then
_stats_sensitive_dirs+=("Geary Flatpak")
_run \
"User only access to Geary email data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/org.gnome.Geary"
fi
if [[ -d "${_home_dir}/.thunderbird" ]]; then
_stats_sensitive_dirs+=("Thunderbird")
_run \
"User only access to Thunderbird email data" \
"chmod -R go-rwx ${_home_dir}/.thunderbird"
fi
if [[ -d "${_home_dir}/.var/app/org.mozilla.Thunderbird" ]]; then
_stats_sensitive_dirs+=("Thunderbird Flatpak")
_run \
"User only access to Thunderbird email data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/org.mozilla.Thunderbird"
fi
if [[ -d "${_home_dir}/.config/Element" ]]; then
_stats_sensitive_dirs+=("Element")
_run \
"User only access to Element chat data" \
"chmod -R go-rwx ${_home_dir}/.config/Element"
fi
if [[ -d "${_home_dir}/.var/app/im.riot.Riot" ]]; then
_stats_sensitive_dirs+=("Element Flatpak")
_run \
"User only access to Element chat data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/im.riot.Riot"
fi
if [[ -d "${_home_dir}/.config/Signal" ]]; then
_stats_sensitive_dirs+=("Signal")
_run \
"User only access to Signal chat data" \
"chmod -R go-rwx ${_home_dir}/.config/Signal"
fi
if [[ -d "${_home_dir}/.var/app/org.signal.Signal" ]]; then
_stats_sensitive_dirs+=("Signal Flatpak")
_run \
"User only access to Signal chat data (Flatpak)" \
"chmod -R go-rwx ${_home_dir}/.var/app/org.signal.Signal"
fi
if [[ -f "${_home_dir}/.config/birthdays.csv" ]]; then
_run \
"User only access to birthday/anniversary reminder data" \
"chmod 600 ${_home_dir}/.config/birthdays.csv"
elif [[ -f "${_BDAY_FILE}" ]]; then
_run \
"User only access to birthday/anniversary reminder data (from variable)" \
"chmod 600 \"${_BDAY_FILE}\""
fi
_run \
"Set the setgid bit to inherit folder permissions" \
"chmod g+s ${_home_dir}"
if hascommand --strict setfacl; then
_run \
"Set user default ACL entries" \
"setfacl -d -m u::rwx ${_home_dir}"
_run \
"Set group default ACL entries" \
"setfacl -d -m g::rx ${_home_dir}"
_run \
"Set others default ACL entries" \
"setfacl -d -m o::--- ${_home_dir}"
fi
# Print summary
echo
echo -e "${BRIGHT_CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${BRIGHT_GREEN}✓ Permission fix complete!${RESET}"
echo -e "${BRIGHT_CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo
if [[ "${_dry_run}" == true ]]; then
echo -e "${BRIGHT_YELLOW}This was a dry run - no actual changes were made${RESET}"
else
echo -e "${BRIGHT_YELLOW}Summary:${RESET}"
echo -e " ${BRIGHT_GREEN}●${RESET} Files processed: ${BRIGHT_CYAN}${_stats_files_processed}${RESET}"
echo -e " ${BRIGHT_GREEN}●${RESET} Directories processed: ${BRIGHT_CYAN}${_stats_dirs_processed}${RESET}"
if [[ ${_stats_scripts_made_exec} -gt 0 ]]; then
echo -e " ${BRIGHT_GREEN}●${RESET} Shell scripts made executable: ${BRIGHT_CYAN}${_stats_scripts_made_exec}${RESET}"
fi
if [[ ${_stats_excluded_dirs} -gt 0 ]]; then
echo -e " ${BRIGHT_GREEN}●${RESET} Directories excluded: ${BRIGHT_CYAN}${_stats_excluded_dirs}${RESET}"
fi
if [[ ${#_stats_sensitive_dirs[@]} -gt 0 ]]; then
echo
echo -e "${BRIGHT_YELLOW}Sensitive directories secured:${RESET}"
for dir in "${_stats_sensitive_dirs[@]}"; do
echo -e " ${BRIGHT_RED}🔒${RESET} ${dir}"
done
fi
fi
echo
else
return 0
fi
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment