ansible.cfg
ansible.cfg is where you set the defaults that would otherwise be a wall of command-line flags and environment variables on every run — the inventory path, the SSH user, how many hosts to hit in parallel, whether to verify host keys. Ansible looks for it in four places and the first one found wins outright, with no merging across files, so a config sitting one directory up can silently override the one you think is in effect.
Knowing the search order is the difference between "my setting isn't taking" and a five-minute fix. There is no layering to reason about, no precedence stack to merge in your head — exactly one file is read per run, and the whole skill is knowing which one and how to confirm it. The settings that matter most shape every run, so you put them in the repo once and let the CI runner and every engineer pick up the same behavior.
The Four-Place Search Order
Ansible checks four locations in a fixed order and stops at the first hit. ANSIBLE_CONFIG, an environment variable naming an explicit file, beats ./ansible.cfg in the current working directory, which beats ~/.ansible.cfg in the home directory, which beats /etc/ansible/ansible.cfg. The first file found is the only file read — settings do not layer across them. If the repo's cfg is found, the system-wide one at /etc/ansible is never even opened.
ANSIBLE_CONFIG env var./ansible.cfg in cwd~/.ansible.cfg/etc/ansible/ansible.cfgThis is the single most useful fact about the file. People reason about it as if a setting in ~/.ansible.cfg would merge with the repo's ansible.cfg and fill in the gaps — it does not. One file wins whole. A setting you expect to inherit from a "lower" file is simply absent if a "higher" file was found first and didn't define it, which is where most "why isn't my setting applying" confusion comes from.
The World-Writable cwd Rule
There is one trap inside that order. Ansible ignores an ./ansible.cfg if the current directory is world-writable, because a config in a directory anyone can write to is an injection vector — someone could drop a malicious cfg and have your run pick it up. The cfg is skipped silently and you fall through to the next location in the search order. This trips people running playbooks from /tmp, which is world-writable by design, where the local cfg you placed is quietly ignored and the run uses unexpected defaults.
Key Connection Settings
A handful of settings shape every run, and they belong in the repo's cfg rather than repeated on the command line. Under [defaults] you set inventory (the host file or directory), remote_user (the SSH user), host_key_checking (whether to verify host keys), and forks — parallelism, defaulting to 5. Under [ssh_connection] you set pipelining, the single biggest performance win, covered three topics on. Set once in the repo, these give everyone identical behavior with no flags to remember.
[defaults] inventory = inventories/prod remote_user = deploy host_key_checking = true forks = 20 [ssh_connection] pipelining = true
Every value here would otherwise be an -i, a -u, a -f, or an exported environment variable on every invocation. Pinning them in the file at the repo root means the deploy command is just ansible-playbook site.yml and behaves identically whether a teammate runs it locally or the GitHub Actions runner runs it in CI.
Repo-Local Config as the Norm
The pattern that makes all of this predictable is shipping the cfg in the repo and running from the repo root. The larkspur-io/infra repo carries its own ansible.cfg at the root, pointing inventory = inventories/prod and setting forks = 20. Because the CI runner checks out the repo and runs from that directory, the ./ansible.cfg location wins, and every engineer who clones the repo gets the same inventory and the same parallelism without exporting anything or remembering any flags. The config travels with the code, reviewed in the same pull requests.
Inspecting the Effective Config
When a setting doesn't seem to apply, two commands answer "which file won and what is it setting" without guessing. ansible --version prints the config file actually in use at the top of its output — read that line and you know which of the four locations was selected. ansible-config dump --only-changed then shows every setting that differs from the built-in default, so you see exactly what the live file changes rather than scrolling the full default dump. Run both before you start debugging a behavior you can't explain.
- Running
ansible-playbookfrom a different directory than expected and picking up~/.ansible.cfgor/etc/ansible/ansible.cfginstead of the repo's cfg, soforksandinventoryquietly revert to defaults and the deploy is slow or hits the wrong hosts. - Expecting settings from two
ansible.cfgfiles to merge — they do not; the first file found is read whole and the rest are ignored, so a single setting in~/.ansible.cfgshadows the entire repo config if the repo one isn't found first. - Keeping a deploy script that
cds into/tmp(world-writable) before running, so the./ansible.cfgthere is silently skipped and the run uses unexpected defaults with no warning. - Setting
host_key_checking = falseglobally in~/.ansible.cfgto "fix" a prompt, then having no host-key verification anywhere — a setting meant for one throwaway box now disarms MITM protection on prod. - Committing secrets like a
vault_password_filepath or an absolute home path into the repoansible.cfg, so the config only works on one machine and leaks layout into version control.
- Ship an
ansible.cfgat the repo root and run from the repo root, so the cwd config wins and every engineer plus the CI runner share one source of defaults. - Confirm which config is live with
ansible --versionand audit non-default settings withansible-config dump --only-changedbefore debugging "my setting isn't applying." - Keep
host_key_checking = trueand manage known-hosts deliberately rather than disabling verification fleet-wide to silence a prompt. - Never run playbooks from a world-writable directory like
/tmp; the repo-local cfg is ignored there and you lose your intended defaults without a warning. - Keep secrets and machine-specific paths out of the committed cfg, so the same file works for everyone and version control carries no home-directory layout.
.terraformrc / TF_CLI_CONFIG_FILE, the provisioning-side analog
SSH ~/.ssh/config overlaps for connection defaults
Puppet puppet.conf plays the same role for that tool
Knowledge Check
A repo has an ./ansible.cfg and the home directory has a ~/.ansible.cfg. You run a playbook from the repo root with no ANSIBLE_CONFIG set. Which config is used, and how do the two combine?
- Only
./ansible.cfgis read — the first file found wins whole, and the home-directory file is ignored entirely - The two files are deep-merged, with each
./ansible.cfgvalue overriding the matching home-directory one key by key - Only
~/.ansible.cfgis read, because the home directory sits higher in the precedence order than the current working directory - Both files are read in full and any setting that conflicts between the two raises a startup error until you resolve it
A deploy script cds into /tmp, which holds a carefully written ansible.cfg, then runs the playbook. The settings in that file don't take effect. Why?
/tmpis world-writable, so Ansible silently ignores its./ansible.cfgas an injection risk and falls through to the next location- Ansible only honors an
ansible.cfgwhen the current directory is named after the project repository, and/tmpfails that name check - A cfg located under
/tmpis hard-coded to always sit at lower precedence than the system-wide/etc/ansible/ansible.cfg - Config files that live outside an initialized git repository working tree are never read by Ansible at all
A setting you expect isn't applying and you want to know which config file Ansible actually loaded. What tells you?
ansible --versionprints the config file in use, andansible-config dump --only-changedshows what it changes from defaultsansible-playbook --checkprints the active config file path as a header line at the very top before it runs the whole play through in check mode- There is no command that reports it; you must walk the search order and compare each candidate file by hand
ansible-inventory --listdumps the resolved data and names which cfg file Ansible ultimately selected
Someone sets host_key_checking = false in ~/.ansible.cfg to stop a prompt on one throwaway box. What is the consequence?
- Host-key verification is now off for every run that uses that file, removing MITM protection across the fleet — including prod
- Only the one throwaway box skips verification, since the setting is automatically scoped to the single host that triggered the prompt
- Nothing changes at all, because
host_key_checkingis only honored when it is set in the repo-level cfg, never the home one - Ansible refuses to start any run until the setting is moved out of
[defaults]and into the[ssh_connection]section
You got correct