Directories that use everything in them, including your backup file
A whole class of Unix configuration is "every file in this directory counts". Edit one in place and leave a .bak beside it, and the backup becomes live configuration — displayed to users, parsed as a job, or loaded as software.
Archive date is an editorial placement, not the original publication date. Published 17 October 2026; documentation reviewed 17 October 2026. Examples describe proposed checks, not customer incidents or measured product results.
cp file file.bak before editing is such deeply ingrained practice that it does not get
thought about. It is also wrong in a specific and common situation: when the directory the file
lives in is a drop-in directory, where the consuming program’s contract is “use every file
in here”.
In that case the backup is not inert. It is a second live copy of the configuration — usually an older one — and the program has no notion that one of these files is special.
The contract, stated plainly
From man 8 pam_motd, on the directory used to assemble the login message:
The /path/dirname.d directory is scanned and each file contained inside of it is displayed.
Each file. No extension filter, no exclusion of anything that looks like a backup. So a
login banner edited in place, with userinfo.motd.bak-20261017 left beside it, prints twice —
the current text and the superseded text, concatenated, to every user who logs in. The file you
edited is correct. The thing users see is not.
The same contract, different consumer, with worse consequences: environment modules treats every
regular file under MODULEPATH as a loadable module.
$ module avail
matlab/2025a
matlab/2025a.bak-lnhint <-- loads the PRE-EDIT modulefile
That entry is offered to users by tab completion and by module avail, and loading it applies
the old configuration. It is not a cosmetic blemish; it is a shadow copy of software
configuration that silently wins for anyone who selects it. Worse for debugging: a verification
script that resolves “the latest matlab” by sorting the directory listing will pick
2025a.bak-lnhint, test the old file, and report that the change did not take effect — sending
you after a bug that does not exist.
The directories this applies to
Not exhaustive, but these cover most of it:
| Directory | Consumer | What a stray file does |
|---|---|---|
/etc/motd.d/ |
pam_motd |
printed to every user at login |
/etc/cron.d/ |
cron | parsed and run as a job |
/etc/sudoers.d/ |
sudo | grants or denies; visudo -c ignores files with a . in the name |
/etc/profile.d/ |
login shells | sourced — only *.sh and *.csh, so a .bak is inert here |
/etc/yum.repos.d/ |
dnf | an old .repo.bak is not read (needs .repo), but .repo.orig edits are |
/etc/systemd/system/*.d/ |
systemd | only *.conf; other names ignored |
/etc/ld.so.conf.d/ |
ldconfig | only *.conf |
/etc/NetworkManager/conf.d/ |
NetworkManager | only *.conf |
MODULEPATH trees |
environment modules | every regular file is a module |
/etc/logrotate.d/ |
logrotate | every file, unless tabooext excludes it |
The pattern to internalise is that the directories split into two groups: those that filter by
extension (profile.d, *.d/*.conf under systemd, ld.so.conf.d) where a .bak is harmless,
and those that take everything (motd.d, cron.d, logrotate.d modulo taboo lists,
MODULEPATH) where it is not. Guessing which is which is not a strategy; the manual page says,
and it is a two-minute check.
/etc/cron.d deserves singling out. A superseded crontab left in place keeps running. If you
“paused” a job by commenting out its line in the current file, and a .bak- copy of the
uncommented version is still sitting in the directory, the job continues on schedule and the
file you are reading says it should not. That is a genuinely difficult thing to diagnose from
the current configuration, because the current configuration is correct.
Where backups should go
Three options, in order of preference:
1. Out of the directory entirely.
BK=/root/config-backups; mkdir -p "$BK"
cp -a /etc/motd.d/userinfo.motd "$BK/userinfo.motd.$(date +%Y%m%d-%H%M%S)"
2. A hidden subdirectory, when the consumer skips dotted entries — true for environment
modules, which ignores .-prefixed names. Preserve the relative path so a restore is a single
mv:
M=/usr/share/Modules/modulefiles
install -D "$M/matlab/2025a" "$M/.backups/matlab/2025a"
Verify the exclusion rather than trusting it. Drop a probe in and confirm it does not appear:
mkdir -p "$M/.backups" && echo '#%Module' > "$M/.backups/ZZPROBE"
module avail -t 2>&1 | grep -c ZZPROBE # expect 0
rm -f "$M/.backups/ZZPROBE"
3. Version control, and no backup file at all. The reason the .bak habit exists is that
there is no other way back. Where the configuration is in a repository, there is, and the
cleanest drop-in directory is one that only ever contains live files.
One caveat on hidden subdirectories: put them where the shared copy lives. If a tree is a
mount point — a network or parallel filesystem mounted at a path whose parent is local disk — a
sibling backup directory created one level up is node-local, so your backups scatter across
whichever host you happened to run the edit on. Check with findmnt before choosing the
location.
Verify the consumer’s output, not your file
The general lesson is the one that makes this class of bug expensive: every check performed on the file was green, because the file was right. The fault was in the directory, which nothing was looking at.
So assert on what the consumer produces:
# how many files will pam_motd actually print?
ls -1 /etc/motd.d | wc -l # expect exactly 1
# does the module system offer anything it should not?
module avail -t 2>&1 | grep -c '\.bak' # expect 0
# what does cron believe it has been told to run?
grep -rhE '^[^#]' /etc/cron.d/ | sort
And when you are checking whether an edit took effect, be careful that the probe itself does not
select the backup. Resolving “the newest version in this directory” by ls | tail -1 is exactly
how a .bak- suffix gets tested instead of the real file.
Symptom against cause
| Symptom | Cause | Check |
|---|---|---|
| Login banner shows old and new text | .bak in /etc/motd.d |
ls -1 /etc/motd.d |
| A “paused” cron job still runs | Superseded file in /etc/cron.d |
grep -rhE '^[^#]' /etc/cron.d/ |
module avail lists *.bak* |
Backups under MODULEPATH |
move to .backups/, verify with a probe |
| Edit appears to have no effect | Test resolved to the backup file | print the path the test used |
| Backups present on one node only | Backup dir on a local parent of a shared mount | findmnt --target |
| sudo rule ignored | Filename contains a . |
rename; visudo -c |
A directory is an interface. If its contract is “everything in here”, then writing a file into it is a configuration change, whatever you intended by the filename.