chmod 755, chmod 644, umask 022. If these numbers feel like incantations, it’s because they are written in octal, which hides the bits underneath. Once you see the bits, Unix permissions turn out to be one of the cleanest real-world examples of bit flags.
Nine Bits, Three Groups
Every file has nine basic permission bits, arranged as three groups of three:
| Group | Read | Write | Execute |
|---|---|---|---|
| owner (user) | bit 8 | bit 7 | bit 6 |
| group | bit 5 | bit 4 | bit 3 |
| others | bit 2 | bit 1 | bit 0 |
Within each group, the values are read = 4, write = 2, and execute = 1. Three bits fit exactly into one octal digit (0–7), which is why permissions are written in octal rather than decimal or hex.
Decoding chmod 755
Convert each octal digit to three bits:
7 5 5
111 101 101
rwx r-x r-x
owner group others
So 755 means the owner can read, write, and execute, while the group and everyone else can read and execute. As a single binary number it is 111101101, which is 493 in decimal and 0x1ED in hex. All of those describe the same nine bits.
Common modes:
| Octal | Binary | Symbolic | Typical use |
|---|---|---|---|
755 | 111 101 101 | rwxr-xr-x | programs, directories |
644 | 110 100 100 | rw-r--r-- | regular files |
600 | 110 000 000 | rw------- | private keys, secrets |
700 | 111 000 000 | rwx------ | private directories |
777 | 111 111 111 | rwxrwxrwx | almost never a good idea |
Building Permissions With OR
Each permission is a single bit, so you combine them with bitwise OR. In C, the constants come from <sys/stat.h>:
#include <sys/stat.h>
mode_t mode = S_IRUSR | S_IWUSR | S_IXUSR /* 0700 */
| S_IRGRP | S_IXGRP /* 0050 */
| S_IROTH | S_IXOTH; /* 0005 */
/* mode == 0755 */
chmod("script.sh", mode);
The leading 0 in 0755 makes it an octal literal in C. Python writes it as 0o755. Forgetting the prefix is a classic bug: chmod(path, 755) in C passes decimal 755, which is octal 1363, a completely different set of bits including the sticky bit.
Checking a Permission With AND
To test a single permission, AND the mode with its bit:
import os, stat
mode = os.stat("script.sh").st_mode
if mode & stat.S_IXUSR:
print("owner can execute")
print(oct(mode & 0o777)) # e.g. '0o755'
print(stat.filemode(mode)) # e.g. '-rwxr-xr-x'
st_mode also contains the file type in its high bits, so mask with 0o777 (or 0o7777 to include the special bits) to see only the permissions. You can try it in the Bitwise AND Calculator by entering the mode and the mask in binary.
umask: AND With NOT
When a program creates a file, it asks for a mode, usually 0666 for files or 0777 for directories. The umask then removes bits:
final_mode = requested_mode & ~umask
With the common umask 022:
requested 666 = 110 110 110
umask 022 = 000 010 010
~umask = 111 101 101
result 644 = 110 100 100
The umask clears the write bit for group and others. It isn’t subtraction. 666 − 022 happens to give the same answer here, but with umask 077 subtraction gives 567 while the correct result, 0666 & ~0077, is 600. It’s the “clear bits” mask operation from the Bit Masking Guide.
Symbolic chmod Is Bit Manipulation Too
chmod g+w file sets the group write bit, which is an OR. chmod o-r file clears the others read bit, which is AND with NOT. chmod a=rx replaces the bits outright. The symbolic form is just a friendlier way to describe the same mask operations.
The Special Bits: setuid, setgid, Sticky
Three more bits sit above the nine permission bits, forming a fourth octal digit:
| Octal | Name | Effect |
|---|---|---|
4000 | setuid | executable runs with the file owner’s privileges |
2000 | setgid | runs with the file’s group, or new files in a directory inherit its group |
1000 | sticky | in a shared directory, only a file’s owner can delete it |
/tmp is typically 1777: everyone can write, but the sticky bit stops users deleting each other’s files. ls -l shows these as s or t in the execute positions, for example drwxrwxrwt.
Converting Between Forms
mode = 0o755
print(bin(mode)) # '0b111101101'
print(mode) # 493
print(f"{mode:03o}") # '755'
def symbolic(m: int) -> str:
chars = "rwxrwxrwx"
return "".join(c if m & (1 << (8 - i)) else "-" for i, c in enumerate(chars))
print(symbolic(0o644)) # 'rw-r--r--'
Summary
Unix permissions are twelve bit flags. Octal digits group them in threes, OR builds a mode, AND tests one, and AND with NOT (the umask) removes them. Seeing 755 as 111 101 101 is all it takes to read any permission at a glance. To practise converting between binary, decimal, and hex, see Binary, Decimal and Hex Conversion.