A monthly job for a phone camera packs up last month's clip folder, clips/2026-02 say, and frees the space. The usual way to get that folder name in a shell script is date -d -1month +%Y-%m, or the same thing spelled with a space inside quotes. I ran that line against every day of 2026. On seven days it names the current month, which is the folder the camera is still writing to.
The test
The check is simple. For each day of the year, take the 1st of that month, step back one day, and read the month. Day units have a fixed length, so that answer is the month that really came before. Then compare it with the obvious one-liner, and with the version the GNU manual suggests (more on that below):
#!/usr/bin/env bash # For every day of 2026, compare three ways of naming "last month" # against the month that really came before. set -u bad_rel=0; bad_mid=0; bad_day=0; total=0 d=2026-01-01 while [ "$d" != 2027-01-01 ]; do want=$(date -d "${d%-*}-01 -1 day" +%Y-%m) # the day before the 1st rel=$(date -d "$d -1 month" +%Y-%m) # the obvious way mid=$(date -d "${d%-*}-15 -1 month" +%Y-%m) # the manual's way [ "$rel" != "$want" ] && { bad_rel=$((bad_rel+1)); echo "$d -1 month -> $rel (wanted $want)"; } [ "$mid" != "$want" ] && bad_mid=$((bad_mid+1)) total=$((total+1)) d=$(date -d "$d +1 day" +%F) done echo "days checked: $total" echo "'-1 month' wrong on: $bad_rel days" echo "15th anchor wrong on: $bad_mid days" Run with TZ=UTC, it printed:
2026-03-29 -1 month -> 2026-03 (wanted 2026-02) 2026-03-30 -1 month -> 2026-03 (wanted 2026-02) 2026-03-31 -1 month -> 2026-03 (wanted 2026-02) 2026-05-31 -1 month -> 2026-05 (wanted 2026-04) 2026-07-31 -1 month -> 2026-07 (wanted 2026-06) 2026-10-31 -1 month -> 2026-10 (wanted 2026-09) 2026-12-31 -1 month -> 2026-12 (wanted 2026-11) days checked: 365 '-1 month' wrong on: 7 days 15th anchor wrong on: 0 days So for 358 days of the year the one-liner is fine, which is why it survives code review and a month of testing. On the 29th, 30th and 31st of March, and on the 31st of May, July, October and December, it returns the month you are in.
Why it happens
-1 month does not mean the month before this one. GNU date keeps the day of the month, changes the month number, and then normalizes the result. On 31 March that gives 31 February, which does not exist, so the date rolls forward past the end of February and lands in early March. The manual says so directly:
"These are fuzzy units, as years and months are not all of equal duration."
and
"The fuzz in units can cause problems with relative items."
Its own example steps back from 31 July and says the result might be 1 July, because 31 June does not exist. In 2026, 31 March lands on 3 March: February has 28 days, so 31 February is three days past its end.
The other spellings do not help. All three are the same relative item:
#!/usr/bin/env bash # Do the other spellings behave any differently? for s in "-1 month" "1 month ago" "last month"; do printf '%-12s -> %s\n' "$s" "$(date -d "2026-03-31 $s" +%F)" done -1 month -> 2026-03-03 1 month ago -> 2026-03-03 last month -> 2026-03-03 What the job would have done
Here is the folder choice on 31 March, with the naive line and with the anchored one:
#!/usr/bin/env bash # Which folder would a "pack up last month" job pick on 31 March? set -u mkdir -p demo/clips/2026-02 demo/clips/2026-03 today=2026-03-31 naive=$(date -d "$today -1 month" +%Y-%m) anchored=$(date -d "${today%-*}-15 -1 month" +%Y-%m) echo "naive picks: demo/clips/$naive" echo "anchored picks: demo/clips/$anchored" naive picks: demo/clips/2026-03 anchored picks: demo/clips/2026-02 The naive line picks clips/2026-03. A job that tars that folder and then removes it would archive a partial month and delete the clips recorded so far that day. February is skipped too. If the job runs on the last day of each month, the next run on 30 April asks for March, so February is not packed up at all.
The failure also depends on when the job fires. A job scheduled on the 1st does not see it. A job scheduled for the last day of each month, or one that runs nightly and checks whether the month has changed, sees it every year.
The same bug going forward
Jobs that create next month's folder ahead of time have the mirror problem:
#!/usr/bin/env bash # Same test for "next month", the folder a job might create ahead of time. set -u bad=0 d=2026-01-01 while [ "$d" != 2027-01-01 ]; do want=$(date -d "${d%-*}-15 +1 month" +%Y-%m) got=$(date -d "$d +1 month" +%Y-%m) [ "$got" != "$want" ] && { bad=$((bad+1)); echo "$d +1 month -> $got (wanted $want)"; } d=$(date -d "$d +1 day" +%F) done echo "'+1 month' wrong on: $bad days" 2026-01-29 +1 month -> 2026-03 (wanted 2026-02) 2026-01-30 +1 month -> 2026-03 (wanted 2026-02) 2026-01-31 +1 month -> 2026-03 (wanted 2026-02) 2026-03-31 +1 month -> 2026-05 (wanted 2026-04) 2026-05-31 +1 month -> 2026-07 (wanted 2026-06) 2026-08-31 +1 month -> 2026-10 (wanted 2026-09) 2026-10-31 +1 month -> 2026-12 (wanted 2026-11) '+1 month' wrong on: 7 days From 29 to 31 January, +1 month skips February entirely. A folder made ahead of time would be 2026-03, and February's clips would go into a folder the script did not create.
Leap years move it, not remove it
Only the 29th to the 31st can go wrong (any earlier day exists in every month), so this checks just those days for three years:
#!/usr/bin/env bash # Only days 29-31 can go wrong, so check just those for three years. set -u for y in 2026 2027 2028; do hits=() for m in 01 02 03 04 05 06 07 08 09 10 11 12; do for dd in 29 30 31; do d=$y-$m-$dd date -d "$d" >/dev/null 2>&1 || continue # skip days that don't exist want=$(date -d "$y-$m-01 -1 day" +%Y-%m) got=$(date -d "$d -1 month" +%Y-%m) [ "$got" != "$want" ] && hits+=("${d#*-}") done done echo "$y: ${#hits[@]} wrong days: ${hits[*]}" done 2026: 7 wrong days: 03-29 03-30 03-31 05-31 07-31 10-31 12-31 2027: 7 wrong days: 03-29 03-30 03-31 05-31 07-31 10-31 12-31 2028: 6 wrong days: 03-30 03-31 05-31 07-31 10-31 12-31 In 2028, 29 February exists, so 29 March steps back cleanly and the count drops from seven to six. The other six days stay broken.
The fix
The manual's advice is to step back from the middle of the month:
"To determine the previous month more reliably, you can ask for the month before the 15th of the current month."
The 15th exists in every month, so stepping one month back from it cannot overflow. Wrapped as a function, with the day passed in so it can be tested:
#!/usr/bin/env bash # Name last month's clip folder from any day, including the 29th-31st. set -eu prev_month() { local today=${1:-$(date +%F)} date -d "${today%-*}-15 -1 month" +%Y-%m } for d in 2026-03-31 2026-03-01 2026-01-31 2026-12-31; do echo "$d -> clips/$(prev_month "$d")" done 2026-03-31 -> clips/2026-02 2026-03-01 -> clips/2026-02 2026-01-31 -> clips/2025-12 2026-12-31 -> clips/2026-11 In the year-long test above, the anchored version matched the real previous month on all 365 days. The other safe form is the one the test itself uses as the reference: take the 1st of the current month and subtract one day. Either works. What does not work is any month arithmetic that starts from a day after the 28th.
What I would check in an existing script
- Search for
monthinsidedate -dordate --datestrings. Each hit that starts from the current date can land in the wrong month on up to seven days a year. - If the job deletes anything, test it with the day passed in (
prev_month 2026-03-31) rather than waiting for a month end. - If the job runs on the 1st only, it is safe for now, but the line will break the day someone moves the schedule.
Things I did not test
- Other
dateimplementations. I ran GNU coreutils 8.32 on Linux. BSD and macOSdateuse-v-1minstead of-d, and I did not check how they handle the 31st. - Times of day. Every test passes a date with no time. A relative item applied to the current time near midnight, or across a daylight saving change, adds its own edge cases. The manual notes that crossing a clock change adjusts the result, and I left that alone.
- Other languages. Python, JavaScript and SQL each have their own month arithmetic. The same question applies to each, but none of them is tested here.
Sources. Quotes are from the GNU Coreutils manual node Relative items in date strings, checked against the copy installed with coreutils 8.32. Outputs are pasted unedited from the scripts above.
If you want an old Android phone recording in the background so there are clip folders worth tidying, the app I make is Background Camera RemoteStream on Google Play. Nothing above depends on it.
-1 month keeps the day and lets it overflow, so count back from the 15th, or from the 1st minus a day, not from the 31st.
