AnsibleX · YAML for Reliable AutomationYAML for reliable automation
File modes: quote them
What you'll learn
- State upstream guidance on quoting octal modes and the reason behind it
- Compute the permission bits produced by an unquoted decimal mode
- Explain why a leading zero works sometimes and not always
- Choose between a quoted octal string and a symbolic mode
- Verify the mode that actually landed on a managed node
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
Upstream states the rule in the mode documentation of every file-
touching module, and it is worth quoting verbatim because the wording is
carefully hedged in a way that tells you something:
For consistent results, quote octal numbers (for example,
'644'or'1777') so Ansible receives a string and can do its own conversion from string into number. Adding a leading zero (for example,0755) works sometimes, but can fail in loops and some other circumstances. Giving Ansible a number without following either of these rules will end up with a decimal number which will have unexpected results.
“Works sometimes” is unusual language for reference documentation. This lesson is what is behind it.
The three forms and what they produce
$ ansible-playbook -i localhost, yamltypes.ymlok: [localhost] => (item=file_mode) => {
"msg": "file_mode = 420 (type int)"
}
ok: [localhost] => (item=file_mode_quoted) => {
"msg": "file_mode_quoted = 0644 (type str)"
}
ok: [localhost] => (item=plain_mode) => {
"msg": "plain_mode = 644 (type int)"
}The arithmetic
$ python3 modes.pymode: 0644 -> YAML int 420 numeric= 420 octal=644 perms=-rw-r--r--
mode: 644 -> YAML int 644 numeric= 644 octal=1204 perms=--w----r-T
mode: '0644'-> int('0644',8) numeric= 420 octal=644 perms=-rw-r--r--
mode: 0755 -> YAML int 493 numeric= 493 octal=755 perms=-rwxr-xr-x
mode: 755 -> YAML int 755 numeric= 755 octal=1363 perms=--wxrw--wt
mode: 0600 -> YAML int 384 numeric= 384 octal=600 perms=-rw-------
mode: 600 -> YAML int 600 numeric= 600 octal=1130 perms=---x-wx--T
loop "0420" -> int("0420",8) numeric= 272 octal=420 perms=-r---w----Three things fall out of that table.
mode: 644 is not a permission bug, it is a different file. Decimal
644 is octal 1204: the sticky bit set, the owner able to write but not
read, the group with nothing, others with read. --w----r-T. A config
file written that way is unreadable by the service that needs it and
world-readable to everyone who does not.
mode: 755 is worse. Decimal 755 is octal 1363: --wxrw--wt.
Sticky bit, group write, world write. On a directory that is a genuine
security defect, and on a script it is a file anyone on the box can
modify.
mode: 0644 produces the right answer for the wrong reason. YAML
resolves 0644 as an octal literal to the integer 420, and 420 happens
to be 0o644. The path taken is “text that looked octal, parsed as
octal, used as a raw number” — and it lands on the correct value only
because those two octal interpretations agree.
Why the leading zero “works sometimes”
The documented failure case is loops, and it is worth being precise about what does and does not happen in ansible-core 2.21.
$ ansible-playbook -i localhost, looptype.ymlTASK [Report the type that survived] *******************************************
ok: [localhost] => {
"msg": "templated_mode=420 type=int"
}
TASK [Report that type too] ****************************************************
ok: [localhost] => {
"msg": "concat_mode=mode-420 type=str"
}So mode: "{{ item.mode }}" over a loop whose items carry mode: 0644
keeps the integer 420 and lands on -rw-r--r--. In this version, that
particular loop is not the failure.
The failure is the moment the value stops being a whole-value template:
# The item value is the integer 420 (from an unquoted 0644 in the loop data).
# Prefixing a zero produces the STRING "0420", which is parsed as octal 420.
- name: Set permissions
ansible.builtin.file:
path: "{{ item.path }}"
mode: "0{{ item.mode }}"
loop: "{{ config_files }}"int('0420', 8) is 272, which is 0o420, which is -r---w----. The
file becomes read-only for its owner and write-only for its group. Both
the leading zero and the loop are present, the two interact, and nothing
warns you.
This is what “works sometimes” means in practice: the unquoted form’s
correctness depends on which code path the value happens to travel, and
that path is decided by things — templating, concatenation, a filter, a
vars file merge — that are not visible at the point of the mistake.
What to write instead
# Quoted octal - always parsed as octal, from a string, by the module.
- name: Install the config
ansible.builtin.copy:
src: app.conf
dest: /etc/app/app.conf
owner: root
group: appsvc
mode: '0640'
# Symbolic - readable, and impossible to get wrong by type conversion.
- name: Make the helper executable for its owner only
ansible.builtin.file:
path: /usr/local/bin/app-helper
mode: u=rwx,g=,o=
# In loop data, quote the mode at its source.
vars:
config_files:
- { path: /etc/app/app.conf, mode: '0644' }
- { path: /etc/app/secret.key, mode: '0600' }
- { path: /usr/local/bin/run, mode: '0755' }Symbolic modes have been supported since Ansible 1.8 and accept the
chmod syntax you already know — u+rwx, g-w, o=, a=r. They are
the better choice in one specific situation: when you want to change
some bits and leave the rest alone. An octal mode always sets every
bit.
Verifying what landed
Whatever the playbook says, the file on the host is the fact.
ansible -i inventories/prod webservers -m ansible.builtin.stat \
-a "path=/etc/app/app.conf" | grep -E '"mode"|"pw_name"|"gr_name"'stat returns "mode": "0644" — a string, in the quoted octal form.
That is not an accident of formatting: it is the representation the rest
of Ansible expects, and a useful reminder of what your playbook should
have contained.
Knowledge check
Knowledge check · 4 questions
Q1. A copy task specifies mode: 644 with no quotes. What permissions does the file end up with?
Q2. Which of these forms reliably produce -rw-r--r--? Select all that apply.
Q3. Because ansible-core 2.21 preserves integer types through a whole-value template, unquoted 0644 in loop data is now safe.
Q4. Why does mode: 0644 produce the correct permissions despite being an integer?
Passing score: 75%. Answers are checked in this browser.