← back to latest

See a file's permissions as the number chmod expects

stat -c '%04a %A %U:%G %n' *

anatomy

stat
Prints what the stat(2) system call returns for each path: mode, owner, size, link count, inode, and timestamps. This is the inode itself, not a view of it. ls calls the same system call and then decides what to show you and how to round it; stat lets you decide instead. Run bare, it prints a multi-line block per file, which is fine for one file and unreadable for twenty.
-c
Replace the default block with your own format string, printed once per file with a newline after it. This is GNU syntax. BSD stat on macOS uses -f for the same job, and GNU stat uses -f for something else entirely (filesystem information), so neither spelling travels.
%04a
The permission bits in octal, the same digits you would hand to chmod. Bare %a prints 644 for an ordinary file and 1777 for /tmp, so the column jumps between three and four digits whenever a setuid, setgid, or sticky bit is present. The 0 and 4 are printf flags that GNU stat passes through: pad to four digits with zeros, so every row lines up and the special-bits digit is always in the same place.
%A
The same mode as ls -l prints it, file type character included. Putting it next to the octal is the point: the two columns translate each other, and the type character (d, l, -) tells you whether the number even means what you think it does.
%U:%G
Owner and group names, joined with a literal colon so the result reads the way chown takes it. If a UID has no entry in /etc/passwd, stat prints UNKNOWN; use %u:%g for the raw numbers, which is what you want inside containers where the names do not match the host.
%n
The file name as given on the command line. For symlinks, %N prints the quoted name and the target, the way ls -l shows an arrow.
*
A shell glob. Bash expands it to every non-hidden name in the directory, and stat handles each one in turn. Dotfiles are left out, so ~/.ssh needs an explicit path or .??* alongside the star.

Sample output

0755 drwxr-xr-x deploy:deploy bin
0640 -rw-r----- deploy:www-data config.env
0777 lrwxrwxrwx deploy:deploy current
0600 -rw------- deploy:deploy id_ed25519
0644 -rw-r--r-- deploy:deploy id_ed25519.pub
2775 drwxrwsr-x deploy:www-data releases
0755 -rwxr-xr-x deploy:deploy restart.sh
0666 srw-rw-rw- deploy:deploy shared.sock

One line needs action. shared.sock is a Unix socket (the s in the type column) that every account on the machine can connect to, and chmod 0660 shared.sock closes that. releases shows a 2 in the first column, which is the setgid bit: new files created inside inherit the www-data group, which is usually deliberate on a deploy directory and worth knowing before you “fix” it. The 0777 on current is not a problem. It is a symlink, and Linux ignores a link’s own mode bits.

When you would reach for it

SSH refuses a key with “UNPROTECTED PRIVATE KEY FILE”, a web server returns 403 on one file out of fifty, or a deploy script needs to copy one file’s permissions onto another. Each time, you need the mode as digits rather than the rwx letters, and converting -rw-r----- to 0640 in your head is how people end up running chmod 777 out of frustration. It is also the quickest way to audit a directory before you hand it to another user.

Gotchas

  • macOS ships BSD stat, where -c is an illegal option. The same output comes from stat -f '%Lp %Sp %Su:%Sg %N' *: %Lp is the low (permission) part of the mode in octal, and the S prefix asks for the string form of a field. brew install coreutils provides GNU stat as gstat if you would rather keep one syntax.
  • Running the macOS form on Linux does not fail the way you expect. GNU -f takes no argument, so the format string is treated as a file name and you get cannot read file system information for '%Lp %Sp %N' followed by a block of filesystem statistics for the files that do exist.
  • Symlinks are reported as themselves, not their targets, which is why current shows 0777. Add -L to follow links and see the mode of the file they point at, which is the one that actually controls access.
  • The octal shows the classic mode bits only. If ls -l prints a + after the permissions, an ACL is granting access the number does not mention; getfacl on Linux and ls -le on macOS show the rest. A missing path prints an error and sets exit status 1, but stat still reports every other file you named.

Variants

$ stat -f '%Lp %Sp %Su:%Sg %N' *

macOS and the BSDs. Same columns, different letters, and no zero-padding of the octal

$ find . -type f ! -perm 644 -exec stat -c '%04a %n' {} +

List every file in a tree whose mode is anything other than 0644. The + packs many names into each stat call instead of starting one process per file

$ echo $(( $(date +%s) - $(stat -c %Y server.log) ))

Seconds since a file was last modified. %Y is the mtime as a Unix timestamp, so the age is plain integer arithmetic with no date parsing

lineage

The stat system call is as old as Unix itself and shows up in the First Edition manual from 1971, but for most of Unix history there was no command to call it from the shell. You asked ls, and ls told you what it thought you needed. The GNU stat command, credited to Michael Meskes, arrived in the GNU file utilities around the turn of the millennium and was folded into coreutils when fileutils, textutils, and sh-utils merged in 2003. The BSDs went their own way at roughly the same time: by most accounts Andrew Brown wrote a stat(1) for NetBSD 1.6 in 2002, FreeBSD picked it up, and that is the one Apple ships. The two were designed independently and it shows. They agree on the name and almost nothing else, which is why a script that reads permissions portably still tends to fall back on parsing ls.