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:

GroupReadWriteExecute
owner (user)bit 8bit 7bit 6
groupbit 5bit 4bit 3
othersbit 2bit 1bit 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:

OctalBinarySymbolicTypical use
755111 101 101rwxr-xr-xprograms, directories
644110 100 100rw-r--r--regular files
600110 000 000rw-------private keys, secrets
700111 000 000rwx------private directories
777111 111 111rwxrwxrwxalmost 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:

OctalNameEffect
4000setuidexecutable runs with the file owner’s privileges
2000setgidruns with the file’s group, or new files in a directory inherit its group
1000stickyin 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.