Skip to content

Fix three safety bugs: trash retention, silent flag typos, blunt chown -R#1

Merged
fschrhunt merged 6 commits into
mainfrom
claude/safety-fixes
Jul 3, 2026
Merged

Fix three safety bugs: trash retention, silent flag typos, blunt chown -R#1
fschrhunt merged 6 commits into
mainfrom
claude/safety-fixes

Conversation

@fschrhunt

Copy link
Copy Markdown
Owner

Summary

Three safety bugs found in a security review, each fixed as a separate commit:

  • Trash retention keyed off file mtime, not deletion date (data loss). vole clean advertises a 7-day grace period before permanently deleting trashed items, but retention was computed from each item's original mtime. Trashing a 2-year-old folder and running vole clean an hour later would permanently rm -rf it. move_to_trash() now records a deletion timestamp when items are trashed (a sidecar file under ~/.Trash/.vole-trash-meta on macOS; the DeletionDate gio already writes into the freedesktop .trashinfo sidecar on Linux, which was previously being ignored). clean.sh's trash sweep now expires items based on that recorded deletion time. Items with no known deletion time (pre-existing trash, or dropped in by another tool) are never deleted on the run that first notices them — a timestamp is backfilled instead, starting the grace period from that point forward.

  • Mistyped flags silently fell through in destructive commands (data loss). clean.sh, optimize.sh, and purge.sh ignored unrecognized flags in their arg-parsing loops — a mistyped --dryrun (instead of --dry-run) ran a REAL destructive pass with no warning. purge.sh was the worst offender, with an explicit -*) ;; no-op that discarded any unknown flag on purpose. update.sh already guarded against this with an explicit -*) echo "Unknown option..."; exit 1 ;; case; the same pattern is now applied to the three destructive scripts, so an unrecognized flag is a hard error instead of a silent fallthrough.

  • Blunt sudo chown -R "$HOME" could flatten container storage. vole optimize's "Home Directory Ownership" task ran a single top-level owner check that, if it failed, triggered sudo chown -R "$(id -u):$(id -g)" "$HOME" — rewriting ownership everywhere under $HOME, including rootless container storage (e.g. ~/.local/share/containers for podman, ~/.local/share/docker for rootless Docker) that is intentionally subuid/subgid-mapped. The repair now walks $HOME's top-level entries, chowns (recursively) only entries whose owner actually doesn't match, and skips known container storage directories entirely. $HOME itself is fixed non-recursively.

Verification

  • shellcheck bin/*.sh lib/core/common.sh lib/optimize/tasks.sh — no new findings introduced (remaining output is pre-existing SC1091/SC2088/SC2034 noise already present on main).
  • bash -n on all modified/related scripts — syntax clean.
  • Bug 2 (mistyped flags): ran each of clean.sh --dryrun, optimize.sh --dryrun, purge.sh --dryrun (misspelled) against a throwaway $HOME. All three now print Unknown option: --dryrun and exit 1 instead of running a real destructive pass. Confirmed correctly-spelled flags (--dry-run, --list, --help, --min=) still work and exit 0.
  • Bug 1 (trash retention): in a throwaway $HOME, created a file with a 2.5-year-old mtime, trashed it via move_to_trash, and confirmed the recorded deletion timestamp was "now" (not the old mtime). Ran clean.sh with a 7-day retention window — the item correctly survived (mtime is irrelevant now). Separately, backdated a trash item's deletion timestamp to 10 days ago (with a fresh mtime) and confirmed clean.sh correctly deleted it — proving retention now keys off deletion time in both directions. Also verified a legacy trash item with no deletion-time record survives its first clean.sh run (conservative backward-compat) and gets a timestamp backfilled. Additionally verified the Linux .trashinfo DeletionDate ISO-8601 parsing logic in isolation with GNU date -d (installed via brew install coreutils since testing ran on macOS).

Not merging — opening for review per instructions.

fschrhunt and others added 3 commits July 3, 2026 02:49
'vole clean' advertises a 7-day grace period before permanently deleting
trashed items, but retention was computed from each item's original
modification time. Trashing a 2-year-old folder and running clean an hour
later would permanently rm -rf it, despite the grace period.

move_to_trash() now records a deletion timestamp when items are trashed:
a sidecar file under ~/.Trash/.vole-trash-meta on macOS, and by relying on
the DeletionDate gio already writes to the freedesktop .trashinfo sidecar
on Linux (which was being ignored in favor of mtime). clean.sh's trash
sweep now expires items based on that recorded deletion time.

Items with no known deletion time (trashed before this existed, or dropped
in by another tool) are never deleted on the run that first notices them —
a timestamp is backfilled instead, starting the grace period from that
point forward.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… them

clean.sh, optimize.sh, and purge.sh silently swallowed unknown flags in
their argument-parsing loops. A mistyped --dryrun (instead of --dry-run)
was ignored rather than rejected, so the command ran a REAL destructive
pass with no warning. purge.sh was worst: it had an explicit `-*) ;;`
no-op case that discarded any unrecognized flag on purpose.

update.sh already guarded against this (an explicit `-*) echo "Unknown
option..."; exit 1 ;;` case). Apply the same pattern to the three
destructive scripts so an unrecognized flag is a hard error (non-zero
exit) instead of a silent fallthrough.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
opt_linux_home_ownership() (vole optimize's "Home Directory Ownership"
task) ran `sudo chown -R "$(id -u):$(id -g)" "$HOME"` whenever $HOME's
top-level owner didn't match the current user. That recursive chown also
rewrites ownership inside rootless container storage under $HOME (e.g.
~/.local/share/containers for podman, ~/.local/share/docker for rootless
Docker), which is intentionally subuid/subgid-mapped — corrupting it.

The repair now walks $HOME's top-level entries, chowns (recursively)
only the ones whose owner actually doesn't match, and skips known
container storage directories entirely. $HOME itself is fixed
non-recursively. No entry is touched unless it's actually misowned, and
container storage is never touched at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ada09b9d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread lib/optimize/tasks.sh Outdated
[[ "$skip" == "true" ]] && continue
entry_owner=$(get_file_owner "$entry")
[[ -z "$entry_owner" || "$entry_owner" == "$USER" ]] && continue
if sudo chown -R "$(id -u):$(id -g)" "$entry" 2> /dev/null; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prune excluded container paths before chowning parents

When the misowned top-level entry is a parent of the excluded storage, e.g. $HOME/.local, the exact-match skip above does not fire because find only yields $HOME/.local at maxdepth 1. This recursive chown therefore still descends into $HOME/.local/share/containers or Docker storage and rewrites rootless container subuid/subgid mappings, which is the corruption this change is trying to avoid; prune those paths from the recursive chown or use find with exclusions.

Useful? React with 👍 / 👎.

Comment thread lib/core/common.sh
deletion_date=$(sed -n 's/^DeletionDate=//p' "$trashinfo" 2> /dev/null | head -1)
[[ -n "$deletion_date" ]] || return 0
epoch=$(date -d "$deletion_date" +%s 2> /dev/null || echo "")
[[ "$epoch" =~ ^[0-9]+$ ]] && echo "$epoch"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat invalid trash timestamps as unknown

When a Linux .trashinfo file has a DeletionDate that date -d cannot parse, this final [[ ... ]] returns 1. Because bin/clean.sh runs with set -e and assigns deleted_epoch=$(trash_deleted_epoch "$item"), one malformed trash sidecar aborts the whole clean instead of taking the unknown-time backfill path; return success with no output for invalid timestamps, the same way missing timestamps are handled.

Useful? React with 👍 / 👎.

Comment thread lib/core/common.sh
local name
name="$(basename "$trashed_path")"
if is_macos; then
local meta_file="$HOME/.Trash/$VOLE_TRASH_META_DIR_NAME/$name"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Key macOS trash metadata by more than basename

On macOS, this lookup reuses any sidecar with the same basename, even if the original trash item was restored or deleted outside Vole and the sidecar remained. If a new item with that basename is later put in Trash by Finder or another tool, clean can apply the stale old epoch and permanently delete the new item on its first run, bypassing the intended grace period; include a unique file identity or clear orphaned sidecars before trusting them.

Useful? React with 👍 / 👎.

Comment thread lib/optimize/tasks.sh Outdated
else
failed=$((failed + 1))
fi
done < <(find "$HOME" -mindepth 1 -maxdepth 1 -print0 2> /dev/null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Repair $HOME before walking inaccessible children

When the home directory itself is misowned and not searchable by the user, this unprivileged find returns no entries while its error is discarded. The script then only chowns $HOME non-recursively and reports that ownership is already optimal, leaving the root-owned contents broken until a second optimize run; chown $HOME before walking it or enumerate with sudo.

Useful? React with 👍 / 👎.

Comment thread lib/core/common.sh Outdated
Comment on lines +442 to +446
{
echo "[Trash Info]"
echo "Path=$trashed_path"
echo "DeletionDate=$iso"
} > "$info_dir/$name.trashinfo" 2> /dev/null || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve existing trashinfo restore paths

When a Linux .trashinfo sidecar exists but lacks DeletionDate, trash_deleted_epoch treats it as unknown and this block rewrites the whole sidecar with Path set to the internal Trash/files location. Desktop trash tools use Path as the original restore target, so a clean run can destroy the only restore location metadata for that item; preserve the existing Path and only add the backfilled timestamp, or keep Vole metadata separately.

Useful? React with 👍 / 👎.

@fschrhunt

fschrhunt commented Jul 3, 2026

Copy link
Copy Markdown
Owner Author

@cursor can you review these findings and if all are useful implement codex's findings.

@cursor

cursor Bot commented Jul 3, 2026

Copy link
Copy Markdown

Reviewed Codex’s findings; all five were useful and are implemented.

Changes made:

  • macOS trash metadata now records/verifies file identity, so stale basename-only sidecars are treated as unknown.
  • Invalid Linux DeletionDate values now return “unknown” instead of aborting clean.sh.
  • Linux trash backfill preserves existing .trashinfo restore Path and only adds/replaces DeletionDate.
  • Home ownership repair now fixes $HOME before traversal.
  • Recursive ownership repair now uses find with explicit container-storage pruning, including when the misowned entry is a parent like ~/.local.

Verification:

  • bash -n passed for bin/*.sh, lib/core/common.sh, and lib/optimize/tasks.sh.
  • Targeted trash metadata regressions passed.
  • Targeted ownership-prune helper regression passed.
  • shellcheck was not installed in this environment.
  • Working tree is clean.

Committed and pushed the updates to the current branch.

Open in Web Open in Cursor 

cursoragent and others added 3 commits July 3, 2026 14:27
Co-authored-by: Fischer <fschrhunt@users.noreply.github.com>
Co-authored-by: Fischer <fschrhunt@users.noreply.github.com>
Co-authored-by: Fischer <fschrhunt@users.noreply.github.com>
@fschrhunt
fschrhunt merged commit 302d04a into main Jul 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants