Every Linux and macOS developer has typed chmod 755 on autopilot — but what does 7 actually mean? Why is 644 the default for files? And what happens to the execute bit when you set the setuid flag?
Most chmod reference pages list a table. I wanted something interactive: flip a checkbox, see the octal update. Type rwsr-xr-x, watch the checkboxes respond. So I built one — in the browser, no server, no framework.
👉 https://chmod-calculator.pages.dev
What It Does
- Checkboxes for Owner / Group / Others × Read / Write / Execute — check one, everything updates instantly
-
Numeric input (e.g.
755) — type any valid octal, the UI reflects it -
Symbolic input (e.g.
rwxr-xr-x) — edit the string, checkboxes follow -
Octal display (e.g.
0755) with the full 4-digit form -
Binary display (e.g.
111 101 101) — see exactly which bits are set - Special bits — Setuid (4000), Setgid (2000), Sticky (1000) as labeled cards with descriptions
- Presets — one click for 644, 755, 777, 600, 700, 400, 4755, 1777
- chmod command generator with an editable filename field and a copy button
- Zero external dependencies — single HTML file, works offline
The Core Logic
Unix permissions are a 12-bit integer. The lower 9 bits are the familiar rwx groups. Bits 9–11 are the special flags.
Bit layout (octal prefix → lower 9 bits):
bit 11 (0x800) = setuid
bit 10 (0x400) = setgid
bit 9 (0x200) = sticky
bits 8-6 = owner r/w/x (0x100, 0x80, 0x40)
bits 5-3 = group r/w/x (0x20, 0x10, 0x08)
bits 2-0 = others r/w/x (0x04, 0x02, 0x01)
Numeric → Symbolic
function modeToSymbolic(mode) {
const bits = [
[0x100, 'r'], [0x80, 'w'], [0x40, 'x'],
[0x20, 'r'], [0x10, 'w'], [0x08, 'x'],
[0x04, 'r'], [0x02, 'w'], [0x01, 'x']
];
let sym = '';
for (let i = 0; i < 9; i++) {
sym += (mode & bits[i][0]) ? bits[i][1] : '-';
}
const setuid = !!(mode & 0x800);
const setgid = !!(mode & 0x400);
const sticky = !!(mode & 0x200);
// Special bits replace the execute position
if (setuid) sym = sym.slice(0, 2) + ((mode & 0x40) ? 's' : 'S') + sym.slice(3);
if (setgid) sym = sym.slice(0, 5) + ((mode & 0x08) ? 's' : 'S') + sym.slice(6);
if (sticky) sym = sym.slice(0, 8) + ((mode & 0x01) ? 't' : 'T');
return sym;
}
The special bit substitution follows the POSIX convention: lowercase s/t when the execute bit is also set, uppercase S/T when it is not. This lets ls -l pack two pieces of information into one character.
Symbolic → Numeric
function symbolicToMode(sym) {
if (!/^[rwxsStT-]{9}$/.test(sym)) return null;
const values = [0x100, 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01];
let mode = 0;
for (let i = 0; i < 9; i++) {
const c = sym[i];
if (c === 'r' || c === 'w' || c === 'x') {
mode |= values[i];
} else if (c === 's') {
mode |= values[i]; // execute bit
if (i === 2) mode |= 0x800; // setuid
else if (i === 5) mode |= 0x400; // setgid
} else if (c === 'S') {
if (i === 2) mode |= 0x800; // setuid without execute
else if (i === 5) mode |= 0x400;
} else if (c === 't') {
mode |= values[i]; // others execute
mode |= 0x200; // sticky
} else if (c === 'T') {
mode |= 0x200; // sticky without execute
}
}
return mode;
}
Position matters: s at index 2 is setuid, s at index 5 is setgid. The regex guard [rwxsStT-]{9} rejects anything that can't be a valid symbolic string before any bit-twiddling begins.
Why s vs S — and t vs T
chmod 4755 /usr/bin/sudo → rwsr-xr-x (setuid, owner exec set)
chmod 4644 myfile → rwSr--r-- (setuid, owner exec NOT set)
chmod 1777 /tmp → rwxrwxrwt (sticky, others exec set)
chmod 1776 /tmp → rwxrwxrwT (sticky, others exec NOT set)
The uppercase form is a warning: the special bit is set but has no execute bit to "attach" to — which is unusual and often unintentional.
Octal formatting
function modeToOctal(mode) {
const special = (mode >> 9) & 7;
const owner = (mode >> 6) & 7;
const group = (mode >> 3) & 7;
const other = mode & 7;
// Prefix 0 when no special bits, otherwise lead with special digit
return special ? `${special}${owner}${group}${other}` : `0${owner}${group}${other}`;
}
0644 and 4755 both have 4 characters. The leading 0 for a plain mode signals "no special bits" without adding a fifth digit.
Binary representation
function modeToBinary(mode) {
const owner = ((mode >> 6) & 7).toString(2).padStart(3, '0');
const group = ((mode >> 3) & 7).toString(2).padStart(3, '0');
const other = (mode & 7).toString(2).padStart(3, '0');
return `${owner} ${group} ${other}`;
}
// 755 → "111 101 101"
// 644 → "110 100 100"
Three groups of three bits, each padded to width 3. This makes the r/w/x mapping visually obvious — a 1 in position 0 of each group means read, position 1 means write, position 2 means execute.
Keeping All Representations in Sync
The state is a single integer currentMode. Every input (checkbox, numeric field, symbolic field, preset button) calls updateFromMode(mode) after computing the new mode value. That function pushes the new state to every output element — no separate reconciliation loop needed.
let currentMode = 0o644;
function updateFromMode(mode) {
currentMode = mode;
// Update all 12 checkboxes
document.getElementById('ur').checked = !!(mode & 0x100);
// ... and the rest
// Update numeric input
document.getElementById('numInput').value = modeToOctal(mode).replace(/^0/, '');
// Update symbolic input
document.getElementById('symInput').value = modeToSymbolic(mode);
// Update display cards and chmod command
}
A suppressUpdate flag prevents re-entrant updates when the function itself sets input values (which would otherwise trigger their input event handlers).
Common Permissions Reference
| Mode | Symbolic | Typical use |
|---|---|---|
| 644 | rw-r--r-- | Regular files (web content, configs) |
| 755 | rwxr-xr-x | Directories, executables |
| 600 | rw------- | Private keys, .env files |
| 700 | rwx------ | Private directories |
| 400 | r-------- | Read-only secrets |
| 4755 | rwsr-xr-x | Setuid executables (sudo, ping) |
| 1777 | rwxrwxrwt | Sticky directories (/tmp) |
Testing Without a Framework
222 tests, no test runner, no dependencies — just Node.js built-ins:
let passed = 0, failed = 0;
function eq(a, b, label) {
if (a === b) { console.log(` ✓ ${label}`); passed++; }
else {
console.error(` ✗ ${label}\n got: ${JSON.stringify(a)}\n expected: ${JSON.stringify(b)}`);
failed++;
}
}
// Round-trip test: mode → symbolic → mode
function roundTrip(mode, label) {
const sym = modeToSymbolic(mode);
const back = symbolicToMode(sym);
eq(back, mode, `round-trip ${label}`);
}
roundTrip(0o755, '755');
roundTrip(0o4755, '4755 setuid');
roundTrip(0o1777, '1777 sticky');
// ... 18 round-trips total
The round-trip tests (symbolic → numeric → symbolic → numeric) catch any asymmetry between the encode and decode paths. If they both have the same bug, round-trips still pass — so I also test both directions against known expected values.
All 222 tests PASSED
Try It
https://chmod-calculator.pages.dev
Source is a single self-contained HTML file — open DevTools, read the script block, and you have the full implementation. Part of devnestio, a collection of browser-only developer tools.
Top comments (0)