Skip to content

Ansible lookup('file', ...) Relative Paths Resolve Against playbook_dir, Not Your Shell's cwd

Problem

A task reads a file with a path relative to the repo root, expecting it to resolve the same way a shell command run from that directory would:

- name: Read this host's checked-in config
  ansible.builtin.set_fact:
    found_value: "{{ lookup('file', 'roles/myrole/files/hosts/' + inventory_hostname + '/some.conf', errors='ignore') }}"

Running ansible-playbook some.yml from the repo root, this either:

  • Returns an empty/None value with errors='ignore' silently swallowing the real problem, or
  • Without errors='ignore', fails with Unable to access the file '<path>': File not found. Use -vvvvv to see paths searched.

— even though the file genuinely exists at that path relative to the repo root, and even though cd <repo-root> && ls <path> confirms it.

Root cause

The file lookup plugin resolves a relative path against the directory containing the playbook file that is currently executing (playbook_dir), not the shell's current working directory the ansible-playbook command was invoked from, and not any role's own directory either, unless the lookup happens to run from a task file in that role whose directory is also playbook_dir for that run.

This is easy to miss because the two usually coincide: cd ~/MajorAnsible && ansible-playbook foo.yml makes playbook_dir equal to the shell's cwd, so a relative path "just works" by accident. It stops working the moment that assumption breaks — e.g. a quick throwaway test playbook written to /tmp for debugging, or invoking a playbook by absolute path from a different directory.

Confirmed with a minimal reproduction (2026-09-30)

# /tmp/test.yml  (playbook_dir = /tmp)
- hosts: localhost
  gather_facts: false
  tasks:
    - debug:
        msg: "{{ lookup('file', 'roles/fail2ban/files/hosts/majorlinux/jail.local') }}"

Run as ansible-playbook /tmp/test.yml from inside ~/MajorAnsible — fails with "File not found", because playbook_dir is /tmp, not the shell's cwd (~/MajorAnsible). The same lookup succeeds immediately once the relative path is replaced with playbook_dir + '/roles/fail2ban/files/hosts/majorlinux/jail.local', or once the test playbook is moved into ~/MajorAnsible itself (making playbook_dir match the intended root).

Fix

Prefix the relative path with the playbook_dir special variable so resolution doesn't depend on where the playbook happens to live relative to the shell's cwd:

- name: Read this host's checked-in config
  ansible.builtin.set_fact:
    found_value: >-
      {{
        lookup('file', playbook_dir + '/roles/myrole/files/hosts/' + inventory_hostname + '/some.conf', errors='ignore')
      }}

This makes the lookup resolve the same way regardless of the shell's current directory — important for a shared repo where playbooks are sometimes run with a full path from outside the repo, or copied into a scratch location for a quick test.

Where this came up in MajorAnsible

Hit while fixing verify_fail2ban.yml's expected_sender check (2026-09-30, see Forgejo-CI): a new task used lookup('file', 'roles/fail2ban/files/hosts/' + inventory_hostname + '/jail.local', errors='ignore') to read each host's real checked-in config instead of comparing against a guessed constant. It silently returned nothing on every host (errors='ignore' masked the real "file not found" error) until a throwaway test playbook written to /tmp reproduced the failure directly and isolated it to playbook_dir vs. the shell's cwd. Prefixing with playbook_dir fixed it immediately; the fix shipped in commit b8abbd0.

See also ansible-regex-search-set-fact-capture-group for a second, unrelated gotcha hit in the same fix.

Quick checks if this still misbehaves

  • Don't debug a lookup('file', ...) path issue with errors='ignore' still in place — it swallows exactly the error message (File not found. Use -vvvvv to see paths searched.) that would otherwise point straight at the problem. Remove it temporarily while diagnosing.
  • A throwaway test playbook's own location matters. Writing a quick reproduction to /tmp to isolate a lookup/path issue works, but remember that playbook's playbook_dir is /tmp too — a path that "should" resolve relative to the real repo won't, in the test, for the same reason it didn't in the original task.
  • role_path is the equivalent special variable when the lookup lives inside a role's own tasks and should resolve relative to that role's directory rather than the top-level playbook's.