Fix chmod mistakes with a permission calculator in 2026
A chmod calculator converts octal modes like 755 into rwx strings (and back), so you stop doing bit math in your head right before running a risky command. Each octal digit is a sum: read is 4, write 2, execute 1. Check the mode before you hit enter, especially with
-R, and remember that directories need the execute bit or nothing inside them can be reached.
The chmod calculator I link to below is one I built. Over the years I'd bounced between five different permission calculators, and every one of them only worked in one direction. You typed a number and got rwx back, never the reverse, so pasting a string from ls -l meant doing the math myself anyway. Mine is free and runs in your browser with no signup. If you know a better one, tell me.
The night chmod -R 644 took down a site
In March 2026, on a Thursday a little after 11pm, I was cleaning up a client's WordPress box after a botched plugin upload. Half the uploads had the wrong ownership. The usual. I typed chmod -R 644 /var/www/html because 644 is the number everyone remembers for web files. Hit enter. Felt productive for about nine seconds.
Then every page returned 403.
If you've done this, you already know why. 644 is rw-r--r--, with no execute bit anywhere. On a regular file that's fine. On a directory, execute means "you're allowed to enter this directory and look up names inside it." Without it, nginx couldn't get into wp-content or anything below it. That was 1,312 files and 214 directories, all suddenly unreachable to the web server user. The files themselves were readable. The path to them wasn't.
The repair is short. You can run find /var/www/html -type d -exec chmod 755 {} + followed by find /var/www/html -type f -exec chmod 644 {} +, or use the one-liner chmod -R u=rwX,go=rX /var/www/html. The capital X adds execute only to directories and to files that already have execute set for someone. (That last part is a trap if your uploads folder already has stray executable files in it, because they'll keep the bit.)
It took me 47 minutes anyway. The fix itself took about 30 seconds. The rest went to second-guessing. I knew 755 was right for directories. I was maybe 90% sure. Honestly, that 10% annoyed me more than the outage did, because I've been doing this for fifteen years and I still convert octal by counting on my fingers at midnight.
That's the whole problem a chmod calculator solves. It's a small, boring translation layer between the number you type and the permissions you meant. You get it right 98 times out of 100. The other two are the ones that page you.
How the octal and rwx math actually works
There's no magic here, which is sort of the point. Unix keeps permission bits in the low 12 bits of a file's mode. The bottom nine split into three groups of three bits each. The first digit is the owner. The second is the group. The third covers everyone else.
Inside each group, read is worth 4. Write is worth 2. Execute is 1. The digit is just the sum, so 7 is everything, 6 is read plus write, 5 is read plus execute, and 4 is read only. That's why 750 reads as "owner can do anything, group can read and enter, others get nothing."
A fourth, leading digit holds the special bits, and it's the one people forget exists. 4 is setuid. 2 is setgid. 1 is the sticky bit. In ls -l output they hijack the execute slot of the matching class. A lowercase s or t means the special bit and execute are both set. An uppercase S or T means the special bit is set without execute, which is almost always a mistake. That's why /tmp is 1777 and shows up as drwxrwxrwt, and why /usr/bin/passwd is typically 4755, displayed as -rwsr-xr-x.
Here's the same logic in Python. I wrote it to check my own tool against the standard library's stat.filemode, which is what produces the string you see in ls-style output. It runs as-is on Python 3.8 or newer:
#!/usr/bin/env python3
"""Convert chmod modes between octal and ls-style rwx strings."""
import stat
def octal_to_symbolic(mode: str) -> str:
value = int(mode, 8)
if not 0 <= value <= 0o7777:
raise ValueError(f"mode out of range: {mode}")
special = (value >> 9) & 0o7
out = []
for i in range(3): # owner, group, other
bits = (value >> (6 - 3 * i)) & 0o7
out.append("r" if bits & 4 else "-")
out.append("w" if bits & 2 else "-")
has_x = bool(bits & 1)
if special & (4 >> i):
mark = "t" if i == 2 else "s"
out.append(mark if has_x else mark.upper())
else:
out.append("x" if has_x else "-")
return "".join(out)
def symbolic_to_octal(perms: str) -> str:
if len(perms) != 9:
raise ValueError("expected 9 characters, like rwxr-xr-x")
special = 0
digits = ""
for i in range(3):
r, w, x = perms[3 * i : 3 * i + 3]
bits = (4 if r == "r" else 0) | (2 if w == "w" else 0)
if x in "xst":
bits |= 1
if x in "sStT":
special |= 4 >> i
digits += str(bits)
return (str(special) if special else "") + digits
if __name__ == "__main__":
for m in ("644", "755", "750", "4755", "2664", "1777"):
print(m, octal_to_symbolic(m))
print("rw-r-----", symbolic_to_octal("rw-r-----"))
# Check all 4096 modes against the standard library's own formatter.
for v in range(0o10000):
sym = octal_to_symbolic(format(v, "o"))
assert sym == stat.filemode(stat.S_IFREG | v)[1:], v
assert int(symbolic_to_octal(sym), 8) == v, v
print("all 4096 modes round-trip")
# Output:
# 644 rw-r--r--
# 755 rwxr-xr-x
# 750 rwxr-x---
# 4755 rwsr-xr-x
# 2664 rw-rwSr--
# 1777 rwxrwxrwt
# rw-r----- 640
# all 4096 modes round-trip
Look at 2664 in that output. rw-rwSr-- has a capital S, because setgid is on while group execute is off. On a directory that combination is usually wrong. On a file it's rarely what anyone intended. I didn't really internalize the uppercase rule until I wrote the assertion loop and watched it fail on my first draft.
If you'd rather not keep a script around, the chmod calculator on aidevhub does the same conversion in both directions. You set the bits visually and read off the number, or paste a string like rwxr-x--- and get 750 back. That reverse direction is the reason I built it. Most of the time I'm staring at ls -l output on a box someone else configured, and I want the number so I can paste it into a ticket or an Ansible task.
Comparing it to the other ways I've done this
I don't think a web calculator is always the right answer. Here's how the options I've actually used stack up:
| Approach | Octal to rwx | rwx to octal | Needs a shell on the box | Good at 2am |
|---|---|---|---|---|
| chmod Calculator (aidevhub) | Yes | Yes | No | Yes, it's visual |
stat on a scratch file |
Yes | Only by chmod-ing a test file and reading it back | Yes | Fine if you remember the flags |
| Mental math | Yes | Slowly | No | Not for me |
| Typical one-way web calculators | Yes | Usually not | No | Mostly, if you can find it under the ads |
| The Python script above | Yes | Yes | Needs Python somewhere | Yes, once it's saved |
The stat route deserves more credit than it gets. On Linux with GNU coreutils, stat -c '%a %A' somefile prints something like 644 -rw-r--r--, which is both forms side by side, straight from the kernel. On macOS the flags differ, and you'd use stat -f '%Lp %Sp' somefile instead. The catch is that it only describes files that already exist. To answer "what would 2750 look like," you have to touch a file, chmod it, and read it back. That works. It's also clunky when you're halfway through writing a deploy script on your laptop and the server is somewhere else.
Mental math is fine for 644, 755 and 600. I've been doing it for years. It falls apart for me on special bits and on reading permissions backwards from a string, which is exactly when the stakes tend to be higher.
When you shouldn't reach for a calculator
A calculator answers one question: what do these bits mean. Plenty of permission problems aren't about the bits at all.
ACLs. If ls -l shows a trailing + after the mode (like -rw-r-----+), the file has POSIX ACL entries that grant access beyond what the nine bits say. No octal number captures that. Run getfacl and read what's actually there.
umask. New files get created with 666 masked by your umask, and directories get 777 masked by it. A umask of 022 gives you 644 files and 755 directories. A calculator will happily tell you what 644 means. It won't tell you why your app keeps creating files as 664. That's a umask in a systemd unit or a shell profile somewhere, and you have to go find it.
Config management. In Ansible, write modes as quoted strings with a leading zero, like mode: "0644". The Ansible docs warn that an unquoted 644 gets read by YAML as a decimal integer, and you end up with a completely different mode than the one you typed. A calculator can't save you from your YAML parser. Symbolic forms like u=rw,g=r,o= sidestep the issue entirely and are easier to review in a pull request anyway.
Filesystems that ignore you. On WSL, files under /mnt/c live on a Windows drive, and chmod mostly does nothing there unless the drive is mounted with the metadata option. Some network mounts and FAT-formatted USB sticks behave the same way. The calculator will show the correct number. The filesystem just won't store it.
Anything security-critical you haven't tested. If you're setting setuid on a binary, please don't trust a web page, mine included. Set it on a test machine, check it with stat, and think hard about whether you need it at all. I'm not sure there's a single setuid binary I've personally shipped in the last five years that I didn't later replace with a capability or a sudo rule.
FAQ
Q: What's the difference between 755 and 775?
A: The middle digit. 755 gives the group read and execute. 775 gives the group write as well, so anyone in the file's group can modify it. On shared deploy directories 775 plus setgid (2775) is common, because new files then inherit the directory's group.
Q: Why does chmod 644 break directories?
A: Directories need the execute bit to be entered and to have names inside them looked up. 644 has no execute bits, so even the owner can't traverse into the directory normally. Use 755 for directories, or chmod -R u=rwX,go=rX to handle files and directories in one pass.
Q: What permissions should an SSH private key have?
A: 600 for the key file and 700 for ~/.ssh. OpenSSH refuses to use a private key that other users can read, and it'll print a big "UNPROTECTED PRIVATE KEY FILE" warning telling you so.
Q: What does a capital S mean in ls -l output?
A: Setuid or setgid is set on that class, but the execute bit isn't. It's usually an accident, often from running chmod 4644 or chmod g+s on something that wasn't executable to begin with.
Written with AI assistance and human review. Try the tool at aidevhub.io/chmod-calculator.
Top comments (0)