linux
umask Explained: Why New Files Are 644 and New Directories 755
Why a new file is 644 and a new directory 755, why umask 023 produces 644 and not 643, why the symbolic form looks backwards, and the commands that ignore your umask completely.
Create a file on any Unix system and it arrives as 644. Create a directory and it arrives as 755. Nobody chose those numbers — no flag was passed, no config file mentions them — and yet they’re the same on your laptop, on the CI runner, and on the server. The number you get is not a default. It’s the result of a subtraction that isn’t a subtraction, applied to a starting value that no program ever writes down. Once you can compute it in your head, a whole category of “why is this file 600 on the server and 644 locally” bugs stops being mysterious.
All of the output below was executed on macOS 26.2 with bash 3.2 and coreutils-equivalent BSD tools; the numbers are identical on Linux unless noted.
The two starting points: 666 and 777
Every program that creates a file passes a requested mode to open(2), and almost every program passes the same one: 0666 for a regular file, 0777 for a directory. That’s rw-rw-rw- and rwxrwxrwx — deliberately maximal, because the kernel is about to take permissions away.
What takes them away is the process’s file mode creation mask, the umask. POSIX defines it in one sentence: “Bit positions that are set in cmask are cleared in the mode of the created file.” The Linux man page gives the same rule as a formula — “permissions in the umask are turned off from the mode argument to open(2) and mkdir(2)” — and notes that “the typical default value for the process umask is S_IWGRP | S_IWOTH (octal 022)”.
Put the three numbers together and the mystery evaporates:
| Requested mode | umask | Result | |
|---|---|---|---|
| File | 666 | 022 | 644 (-rw-r--r--) |
| Directory | 777 | 022 | 755 (drwxr-xr-x) |
$ umask
0022
$ touch f1; mkdir d1; stat -f "%Sp %N" f1 d1
-rw-r--r-- f1
drwxr-xr-x d1
$ umask 077; touch f3; mkdir d3; stat -f "%Sp %N" f3 d3
-rw------- f3
drwx------ d3
$ umask 002; touch f4; mkdir d4; stat -f "%Sp %N" f4 d4
-rw-rw-r-- f4
drwxrwxr-x d4
The three-digit permission result is the thing you can build and read in the chmod calculator — tick the boxes for rw-r--r-- and you get 644. What the calculator can’t do is the masking step; for that, the table below is the shortcut.
It’s a mask, not a subtraction
Almost every tutorial says “umask is subtracted from 666.” That works for the common values and then quietly lies to you. The real operation is a bitwise AND-NOT: mode & ~umask.
Watch it break with umask 023. Subtraction would predict 666 - 023 = 643:
$ umask 023; touch f5; mkdir d5; stat -f "%Sp %N" f5 d5
-rw-r--r-- f5 # 644, not 643
drwxr-xr-- d5 # 754
Digit by digit, the “other” column is 6 (110) masked by 3 (011). Subtraction gives 3. Masking clears the write bit (already the only one that overlaps) and leaves 100 = 4:
| Digit | Requested | Mask | & ~mask | Subtraction would say |
|---|---|---|---|---|
6 = rw- | 110 | 3 = 011 | 100 = 4 (r--) | 3 (-wx) |
7 = rwx | 111 | 3 = 011 | 100 = 4 (r--) | 4 — coincidence |
A mask can only ever clear bits it has set. It can never grant a permission, and it can never produce a permission the requesting program didn’t ask for — which is why umask 023 can’t hand out an execute bit the way naive subtraction implies.
The symbolic form looks backwards because it is
umask accepts and prints a symbolic form, and it is the exact inverse of the numeric one: the numeric form lists the bits you remove, the symbolic form lists the bits you keep.
$ umask 027; umask -S
u=rwx,g=rx,o=
$ umask u=rwx,g=,o=; umask
0077
So umask 022 and umask u=rwx,g=rx,o=rx are the same setting written two ways. If you’ve ever set umask g=rwx expecting to strip group permissions and got the opposite, this is why. Both forms work identically in zsh, which just prints the numeric form without the leading zero.
Where your umask actually comes from
The umask is a property of a process, not of a user or a directory. The man page is explicit: “A child process created via fork(2) inherits its parent’s umask. The umask is left unchanged by execve(2).” Everything follows from that.
- Your shell got it from the login process. On Linux,
pam_umasksets it from, in order, aumask=entry in the user’s GECOS field, aumask=module argument,UMASKin/etc/login.defs, orUMASK=in/etc/default/login.login.defsdocuments the fallback: “The file mode creation mask is initialized to this value. If not specified, the mask will be initialized to 022.” 002on Ubuntu and Fedora is not a distro quirk, it’s the user-private-group rule inpam_umask: “If the user is not root and the username is the same as primary group name, the umask group bits are set to be the same as owner bits (examples: 022 -> 002, 077 -> 007).” That’s why files you create are group-writable on a personal account and aren’t as root.- systemd services don’t read your shell config at all.
UMask=“Controls the file mode creation mask… Defaults to 0022 for system units.” Aumask 007in~/.bashrchas no effect on anything systemd starts. - Anything launched by a GUI, a cron daemon, or a container runtime inherits from that parent, not from your interactive shell. This is the single most common reason a file written by your app has different permissions than the same file written by hand in a terminal.
Every language exposes the same inherited value:
$ umask 027
$ python3 -c "import os; os.mkdir('pyd'); print(oct(os.stat('pyd').st_mode & 0o7777))"
0o750
$ node -e "fs.writeFileSync('nf','x'); console.log((fs.statSync('nf').mode & 0o7777).toString(8))"
640
What ignores your umask
The mask only applies when a file is created with a requested mode. A surprising number of everyday operations sidestep it:
| Operation | Under umask 077 | Why |
|---|---|---|
touch new | 600 | Created with 0666, masked |
echo x > new | 600 | Same — shell redirection creates with 0666 |
echo x > existing | unchanged | Truncation isn’t creation; mode is left alone |
chmod 666 f | 666 | chmod sets modes explicitly; umask never applies |
mkdir -m 755 d | 755 | Explicit mode, applied after creation |
install -m 755 src dst | 755 | Same |
cp src dst | 700 from a 777 source | New file, mode requested from source, then masked |
cp -p src dst | 777 | -p preserves the source mode |
tar xf a.tar | 700 for a 777 member | Modes masked for non-root |
tar xpf a.tar | 777 | -p restores stored permissions |
Measured, not assumed:
$ umask 077
$ mkdir -m 755 dm; stat -f "%Sp %N" dm
drwxr-xr-x dm
$ cp src777 cp777; stat -f "%Sp %N" src777 cp777
-rwxrwxrwx src777
-rwx------ cp777
$ cp -p src777 cpp777; stat -f "%Sp %N" cpp777
-rwxrwxrwx cpp777
$ tar xf a.tar; stat -f "%Sp %N" archived # bsdtar 3.5.3
-rwx------ archived
$ rm archived; tar xpf a.tar; stat -f "%Sp %N" archived
-rwxrwxrwx archived
Zip is the odd one out. With Info-ZIP unzip 6.00, a member stored as 755 came back as 755 even under umask 077 — the stored mode won. If you rely on “extraction is safe because the umask will tighten it,” verify it on the exact tool you ship with, because tar and unzip disagree.
The bits umask can never reach
Files never get an execute bit from creation. The requested mode is 0666, and a mask can only clear. That’s why a script you just wrote is never runnable until you chmod +x it, no matter how permissive the umask:
$ umask 000; touch wide; stat -f "%Sp %N" wide
-rw-rw-rw- wide
setuid, setgid, and sticky are out of scope too. POSIX says “Only the file permission bits of cmask are used; the meaning of the other bits is implementation-defined.” So the leading digit in 0022 is decoration — and asking the kernel for them at creation doesn’t work either:
$ umask 0022
$ python3 -c "import os; fd=os.open('suid', os.O_CREAT|os.O_WRONLY, 0o7777); os.close(fd)"
$ stat -f "%Sp" suid # 0755 — the setuid/setgid/sticky bits were dropped
-rwxr-xr-x
Those special bits are a separate topic entirely, covered in chmod 755 vs 644 along with the recursive-chmod mistake.
Group ownership isn’t a umask question either, and the answer differs by platform. On macOS a new file inherits the directory’s group (BSD semantics); on Linux it gets the process’s effective group unless the directory has the setgid bit:
$ id -gn
staff
$ ls -ld gdir | awk '{print $3, $4}'
kylehurst wheel
$ touch gdir/inner; ls -l gdir/inner | awk '{print $3, $4}'
kylehurst wheel # inherited from the directory, not from my egid
If a shared upload directory keeps producing files the web server can’t read, check the group and the setgid bit before you touch the umask.
The lookup table
| umask | New files | New directories | Typical use |
|---|---|---|---|
000 | 666 | 777 | Never. World-writable everything |
002 | 664 | 775 | Shared group work, user-private-group systems |
022 | 644 | 755 | The standard default |
027 | 640 | 750 | Group reads, world sees nothing |
077 | 600 | 700 | Private: SSH keys, credentials, backups |
023 | 644 | 754 | Proof it’s a mask, not a subtraction |
Debugging checklist
umaskin the shell tells you about that shell, and nothing else. Check the umask of the process that actually wrote the file —cat /proc/<pid>/status | grep Umaskon Linux ≥ 4.7.- If the writer is a systemd unit, set
UMask=in the unit; the shell’s value is irrelevant. - If the file is fine but the directory isn’t traversable, you want the execute bit on the directory — permissions on the path matter as much as on the file.
- If the permissions look right and access still fails, look past the mode bits: ACLs (
ls -leon macOS,getfaclon Linux), SELinux labels, and mount options such asnoexecall override the mode you’re staring at. - Never “fix” it with
chmod -R 777. Fix the creating process’s umask, or set the mode explicitly at creation withinstall -m.
Build and check the target permissions with the chmod calculator, then set them deliberately — and read chmod 755 vs 644 if you want the ls -l columns and the symbolic chmod forms in one place.