mkdir -p creates every missing directory in a path rather than only the last one, and it succeeds silently if the directory already exists, which is what makes it the only form worth using in a script.
Both behaviours come from the same flag and both matter. Without it, mkdir a/b/c fails if a/b does not exist, and fails again if a/b/c already does. With it, the command expresses an intent rather than an action: make sure this path exists.
That difference is the whole reason the flag is in every deployment script ever written.
Table of contents
- The two behaviours
- Permissions, and the umask surprise
- Brace expansion, which saves real time
- In scripts, and what to check afterwards
- The equivalents elsewhere
- How this fits the rest of the stack
- FAQ
The two behaviours
First, it creates intermediate directories.
# Fails if /var/www/example.com does not already exist.
mkdir /var/www/example.com/releases/2026-09-01
# mkdir: cannot create directory ...: No such file or directory
# Creates every level that is missing.
mkdir -p /var/www/example.com/releases/2026-09-01
Second, and just as important, it does not complain when the target already exists.
mkdir /tmp/cache
mkdir /tmp/cache
# mkdir: cannot create directory '/tmp/cache': File exists (exit 1)
mkdir -p /tmp/cache
mkdir -p /tmp/cache
# no output, exit 0 both times
That second property is why it belongs in any script running under set -e. Without the flag, a script that has already run once fails on its second execution at the first directory it created last time, which is a maddening way to discover your deployment is not idempotent.
It is worth being clear about what it does not do: it never modifies or removes an existing directory. If the path exists as a directory, the command is a no-op. If the path exists as a file, the command fails, which is the correct behaviour and occasionally surprising.
Permissions, and the umask surprise
New directories get mode 777 masked by your umask, which usually gives 755. But there is a subtlety specific to -p that catches people.
The -m flag sets the mode of the final directory. It does not apply to the intermediate directories that -p creates, which get the umask default instead.
umask 022
mkdir -p -m 700 /tmp/outer/inner/secret
ls -ld /tmp/outer /tmp/outer/inner /tmp/outer/inner/secret
# drwxr-xr-x /tmp/outer <- umask default, not 700
# drwxr-xr-x /tmp/outer/inner <- umask default, not 700
# drwx------ /tmp/outer/inner/secret
If you intended the whole path to be private, it is not. The parents are world-readable and world-traversable, which means anyone can walk into them and see the names inside.
Two ways to get it right. Set the umask for the duration, or create the levels explicitly:
# Restrictive for everything created here.
(umask 077 && mkdir -p /tmp/outer/inner/secret)
# Or be explicit about each level.
mkdir -m 700 /tmp/outer
mkdir -m 700 /tmp/outer/inner
mkdir -m 700 /tmp/outer/inner/secret
The subshell form is the one to remember, because it keeps the umask change scoped and does not leak into the rest of the script.
Brace expansion, which saves real time
The shell expands braces before mkdir ever runs, which means one command can build an entire tree.
mkdir -p project/{src,tests,docs,scripts}
# Nested, and it multiplies out.
mkdir -p app/{api,worker}/{src,tests}
# app/api/src app/api/tests app/worker/src app/worker/tests
# Ranges work too.
mkdir -p logs/2026/{01..12}
Two things worth knowing. This is a shell feature, not a mkdir feature, so it works with any command and is available in bash and zsh but not in strict POSIX sh. If your script has a #!/bin/sh shebang and runs under dash, brace expansion will not expand and you will create a directory literally named {src,tests,docs}.
And no spaces inside the braces. {src, tests} expands to two words, one of which begins with a space, which is not what anyone wants.
Check what you are about to create before running it on anything important:
echo project/{src,tests,docs,scripts}
In scripts, and what to check afterwards
The idiomatic use in a deployment script is short and worth getting exactly right.
#!/usr/bin/env bash
set -euo pipefail
RELEASE_DIR="/home/deploy/releases/$(date +%Y%m%d%H%M%S)"
SHARED_DIR="/home/deploy/shared"
mkdir -p "$RELEASE_DIR" "$SHARED_DIR"/{log,tmp,uploads}
# -p returns success if the path already exists, so a separate check is
# needed to confirm it is a directory and writable rather than merely present.
[[ -d "$RELEASE_DIR" && -w "$RELEASE_DIR" ]] || {
echo "release directory is not usable: $RELEASE_DIR" >&2
exit 1
}
That final check matters more than it looks. Because -p exits zero when the path already exists, a successful command does not prove the directory is usable. It could exist and be owned by someone else, or be a symlink somewhere unexpected. On a path you did not create in this run, verify rather than assume.
Always quote the variable. An unquoted path containing a space becomes two arguments and creates two directories, neither of which is the one you wanted.
The related trap: mkdir -p on a path where an intermediate component is a file fails with a confusing message about a file existing, which is worth recognising rather than debugging from scratch.
The equivalents elsewhere
The same idea appears in every language’s standard library, usually with a flag that makes it idempotent.
from pathlib import Path
# parents=True is -p; exist_ok=True is the no-error-if-present half.
Path("/var/www/app/releases/current").mkdir(parents=True, exist_ok=True)
import { mkdir } from "node:fs/promises";
// recursive: true covers both behaviours in one flag.
await mkdir("/var/www/app/releases/current", { recursive: true });
In Python, note that the two flags are separate. parents=True alone still raises if the final directory exists, which is the same asymmetry as the shell command and catches people migrating a script.
One general caution across all of these: none of them is atomic. Between checking whether a path exists and creating it, another process can act, which matters in a temporary directory writable by others. Where that is a real concern, use the language’s secure temporary directory facility rather than constructing a path and creating it yourself.
How this fits the rest of the stack
Directory layout is one of those things that lives on a long-lived server and slowly accumulates state nobody documented, which is why deploy scripts grow these defensive commands in the first place. On a platform where each deploy builds a fresh filesystem from the repository, that accumulation has nowhere to happen. The RunxBuild hosting calculator prices that shape, with persistent storage available as an explicit attachment rather than as whatever happens to be left on the disk.
Useful related references:
FAQ
What does the -p flag do in mkdir?
Two things. It creates any missing parent directories in the path rather than only the final one, and it exits successfully if the directory already exists instead of raising an error. The second behaviour is what makes it safe to run repeatedly in a script.
Is mkdir -p safe to run on an existing directory?
Yes. It is a no-op: it never modifies permissions, ownership or contents of a directory that already exists, and it exits zero. The only failure case is when the path exists as a file rather than a directory.
Why do parent directories not get the mode I set with -m?
Because -m applies only to the final directory. Intermediate directories created by -p use the umask default instead. If you intended the whole path to be restricted, run the command inside a subshell with a restrictive umask, or create each level explicitly.
Can I create multiple directories in one command?
Yes, either by listing several paths or with shell brace expansion, such as mkdir -p project/{src,tests,docs}. Brace expansion is a shell feature rather than a mkdir one, so it works in bash and zsh but not under strict POSIX sh.
What is the equivalent in Python or Node?
In Python, Path.mkdir with parents=True and exist_ok=True; both flags are needed, since parents alone still raises when the final directory exists. In Node, fs.mkdir with recursive: true covers both behaviours in a single option.