Hidden Files and ls Options

In Linux, any file or directory whose name begins with a dot (.) is treated as hidden. It exists on disk exactly like any other file, but the shell and most command-line tools quietly leave it out of a normal directory listing. Hidden files are everywhere: your shell’s startup scripts, your Git repository’s metadata, your SSH keys, and countless application settings all live as dotfiles in your home directory. The ls command is your primary window into the filesystem, so knowing its options for revealing, sorting, and formatting hidden files is essential. This lesson covers exactly how hiding works, the full set of useful ls options, and the mistakes people make when they forget hidden files exist.

Overview: What Actually Makes a File “Hidden”?

Unlike Windows, Linux has no special “hidden” attribute stored in the filesystem. A file’s hidden status is determined purely by convention: if its filename starts with a dot, tools like ls and the shell’s own wildcard expansion (globbing) skip over it by default. The kernel does not know or care that a file is “hidden” — when a program calls readdir() to list a directory’s contents, it gets back every entry, dotfiles included. Filtering happens entirely in userspace, inside ls itself.

This convention traces back to early Unix. Every directory contains two special entries: ., which points back to the directory’s own inode, and .., which points to the parent directory’s inode. These are literally how cd .. and cd . work — they are real directory entries, not shell tricks. Early versions of ls skipped any entry starting with a dot so that . and .. wouldn’t clutter every single listing. Programmers noticed this side effect and started naming configuration files with a leading dot on purpose, so their clutter would be hidden the same way. That accidental behavior became a firm, universal convention.

Because hiding is cosmetic rather than a security feature, a dotfile is exactly as readable, writable, and executable as any other file — standard Linux permissions still fully apply. A file named .env with permissions 644 is still world-readable; hiding it from a casual ls does nothing to protect its contents. If a file holds secrets, you still need to lock it down with chmod 600.

Common hidden files you’ll encounter

  • ~/.bashrc and ~/.profile — shell startup configuration
  • ~/.bash_history — your command history
  • ~/.ssh/ — SSH keys and known hosts
  • ~/.gitconfig and a project’s .git/ directory — Git identity and repository metadata
  • ~/.config/ — a standard location many applications use for settings
  • .env — environment variables for a project, often containing credentials

Syntax

The general form of the command is:

ls [OPTION]... [FILE]...

With no arguments, ls lists the current directory. Give it one or more paths to list those instead. The table below covers the options you’ll use constantly.

Option Meaning
-a Show all entries, including dotfiles and the . / .. entries
-A Like -a, but hides . and .. (“almost all”) — usually what you actually want
-l Long format: permissions, link count, owner, group, size, modification time, name
-h With -l, print sizes in human-readable units (K, M, G) instead of raw bytes
-t Sort by modification time, newest first, instead of alphabetically
-r Reverse the current sort order
-R List subdirectories recursively
-d List a directory itself as an entry, instead of descending into it
-F Append a marker to names showing type (/ for directories, * for executables)
-S Sort by file size, largest first
--color=auto Colorize output by file type (usually already the default via a shell alias)

Examples

Example 1: A plain listing hides the dotfiles.

ls ~/projects

Output:

notes.txt  report.pdf  src

This looks like the whole story, but it isn’t. The directory also contains a .git repository and a .env file — ls simply isn’t showing them because neither -a nor -A was given.

Example 2: Revealing hidden entries with -a.

ls -a ~/projects

Output:

.  ..  .env  .git  notes.txt  report.pdf  src

Now every entry is visible, including . (this directory) and .. (its parent). Two previously invisible items appear: .env and .git. This is the moment most beginners realize a directory they thought was simple actually holds a lot more.

Example 3: Combining flags for a detailed, sorted, human-readable view.

ls -laht ~/projects

Output:

total 76K
drwxr-xr-x  5 alice alice 4.0K Aug  3 14:22 .
drwx------  8 alice alice 4.0K Aug  3 14:22 .git
-rw-------  1 alice alice  112 Aug  2 18:03 .env
drwxr-xr-x 12 alice alice 4.0K Aug  1 09:10 ..
-rw-r--r--  1 alice alice  256 Jul 30 11:45 notes.txt
-rw-r--r--  1 alice alice  48K Jul 29 16:20 report.pdf
drwxr-xr-x  3 alice alice 4.0K Jul 28 10:00 src

Reading a long-format line left to right: the first character is the file type (- for a regular file, d for a directory), followed by three permission triplets for owner, group, and other. Then comes the link count, the owner and group names, the size (in human-readable units thanks to -h), the last modification time, and finally the name. Because -t was used, the newest entries (.git and .env, both touched today) appear first instead of alphabetically. Notice too that .git is mode 700 (only the owner can enter it) and .env is mode 600 (only the owner can even read it) — sensible permissions for anything containing repository internals or secrets.

How ls Works Step by Step

When you run ls against a directory, several things happen in sequence:

  1. ls opens the directory with opendir() and repeatedly calls readdir(), which returns every entry the kernel has recorded — filename and inode number for each, dotfiles and all.
  2. Unless -a or -A was passed, ls filters out any entry whose name starts with . before doing anything else. This filtering is purely a userspace decision made by the ls program, not the kernel.
  3. If you asked for extra metadata (-l, -h, -t, -S), ls calls stat() on each remaining entry to fetch its inode information: permission bits, owner and group IDs, size, and timestamps. This is why ls -l on a huge directory is noticeably slower than a bare ls — it’s doing one extra system call per file.
  4. The remaining entries are sorted. The default is alphabetical by filename; -t switches to modification time (newest first), -S to size (largest first), and -r reverses whichever order was chosen.
  5. Finally, ls formats and prints the result — in columns for a bare listing, one entry per line for -l, and with ANSI color codes if --color is active (most distributions enable this by default through a shell alias like alias ls='ls --color=auto').

Common Mistakes

Mistake 1: Assuming rm * empties a directory completely. The shell expands * before rm ever runs, and shell globbing never matches dotfiles by default — the same convention ls follows. Someone trying to fully clear out an old project directory might run this and be surprised later:

cd ~/old-project
rm *

This deletes notes.txt, report.pdf, and src, but .git and .env are left behind untouched, silently consuming disk space and potentially leaving credentials on disk. If the goal is to remove everything — hidden files included — target the directory itself instead of relying on a wildcard:

cd ~
rm -rf ~/old-project

Mistake 2: Trying to list only hidden files with ls .*. This looks reasonable, but the shell expands .* to match every dotfile including . and .. themselves:

ls .*

Because .. is included in that expansion, ls receives the parent directory as one of its arguments and happily lists its entire contents too — producing a confusing mashup of the current directory’s dotfiles and the parent directory’s full listing. Use the purpose-built flag instead, which excludes . and .. automatically:

ls -A ~/projects

Mistake 3: Forgetting -d when you want the entry, not its contents. If you pass a directory name directly to ls, it lists what’s inside that directory, not the directory entry itself:

ls -a .config

Instead of confirming that a .config directory exists, this dumps everything stored inside it — potentially dozens of unrelated application folders. When you want information about the directory itself (its permissions, its size as an entry, or simply to confirm it exists) without descending into it, add -d:

ls -d .config

Best Practices

  • Get in the habit of running ls -a (or -A) before deleting, archiving, or copying a directory — an apparently empty or simple directory often isn’t.
  • Prefer -A over -a in day-to-day use and in scripts; excluding . and .. avoids off-by-two surprises when counting or iterating over entries.
  • Never assume hiding equals security. Set restrictive permissions (chmod 600 for secret files, chmod 700 for private directories) on any dotfile that holds credentials.
  • When auditing what’s really in a directory, combine flags: ls -laht gives you type, permissions, owner, human-readable size, and recency all at once.
  • Don’t try to parse ls output in scripts (filenames can contain spaces, newlines, or dashes that break naive parsing). Use globs, find, or Bash’s own filename expansion instead.
  • If you need shell globs to match dotfiles too, enable the dotglob shell option (shopt -s dotglob) rather than trying to hand-craft a pattern like .[^.]*.

Practice Exercises

  • Create a hidden file called .secrets inside a scratch directory (for example ~/practice). Run a plain ls, then ls -a, then ls -A, and note exactly which entries appear in each case and why.
  • Run ls -ld ~/.ssh (if you have one) and read the permission bits. Explain in your own words why an SSH client refuses to work if that directory is more permissive than 700.
  • Write a one-line command that counts how many hidden files (excluding . and ..) exist directly inside your home directory. Hint: combine ls -A with wc -l, keeping in mind this approach can miscount if any filename contains a newline.

Summary

  • A “hidden” file is just a file whose name starts with . — there is no special kernel-level hidden attribute, and normal permissions still fully apply.
  • . and .. are real directory entries pointing to the current and parent directory’s inodes, not shell shortcuts.
  • ls -a shows everything, including . and ..; ls -A shows everything except those two, which is usually what you want.
  • ls -l adds permissions, ownership, size, and timestamp; add -h for human-readable sizes and -t to sort by recency.
  • Shell wildcards like * never match dotfiles by default, which is why rm * quietly leaves hidden files behind.
  • Use ls -d when you want information about a directory entry itself instead of listing what’s inside it.