The SituationIt's 2 AM and your playbook just died with this:
fatal: [web01]: FAILED! => {
'changed': false,
'cmd': 'some_nonexistent_command',
'msg': '[Errno 2] No such file or directory: b\'some_nonexistent_command\'',
'rc': 2
}
You SSH into the host, run the command manually โ it works fine. So why is Ansible failing?
PATH. Ansible connects via SSH and spawns a non-interactive, non-login shell. That shell skips ~/.bashrc and /etc/profile entirely. Tools installed via nvm, pyenv, rbenv, or custom scripts drop their binaries into user-specific directories โ none of which appear in Ansible's stripped-down PATH.
Debug Process### Step 1: Confirm it's a PATH issue, not a missing installStart by checking whether the command exists on the remote host and where it lives:
- name: Find where the command lives
command: which some_command
register: cmd_path
ignore_errors: true
- name: Show PATH in Ansible shell environment
command: env
register: env_output
- debug:
var: env_output.stdout_lines
Inspect the output. Ansible's PATH is typically /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin โ six entries. Your interactive SSH session might have 12 or more. The missing directory usually jumps out immediately.
Step 2: Check if the command is installed at all```
ansible web01 -m command -a "which some_command" ansible web01 -m command -a "ls /usr/local/bin/some_command"
No output from `which`? The command isn't installed โ skip ahead to Solution 4. Got a path back? It's installed but Ansible can't see it. That's a PATH problem.
### Step 3: See exactly what PATH Ansible gets```
ansible web01 -m command -a "echo $PATH"
Typical Ansible PATH looks like:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Run that same echo $PATH in your interactive SSH session. The discrepancy between the two tells you exactly what's missing.
Solutions### Solution 1: Use the full absolute pathFastest fix when you know where the binary lives:
- name: Run command with full path
command: /usr/local/bin/some_command --arg value
Don't know the path? Resolve it dynamically first:
- name: Get full path
command: which some_command
register: cmd_path
- name: Use resolved path
command: "{{ cmd_path.stdout }} --arg value"
Solution 2: Inject the PATH for the taskThe environment parameter lets you extend PATH per task without touching anything else on the host:
- name: Run with custom PATH
command: some_command --arg value
environment:
PATH: "/opt/custom/bin:/usr/local/bin:/usr/bin:/bin:{{ ansible_env.PATH }}"
Need it everywhere in the play? Set it once at the play level:
- hosts: webservers
environment:
PATH: "/opt/custom/bin:/usr/local/bin:{{ ansible_env.PATH }}"
tasks:
- name: This task now sees the extended PATH
command: some_command
Solution 3: Source the shell profile before runningTools like nvm, pyenv, and rbenv need their init scripts sourced before they work. Switch from the command module to shell and source the profile explicitly:
- name: Run with profile sourced
shell: "source ~/.bashrc && some_command --arg value"
args:
executable: /bin/bash
To load /etc/profile and ~/.bash_profile, invoke bash as a login shell directly:
- name: Run in login shell
shell: bash -l -c 'some_command --arg value'
Important: This only works with the shell module. The command module executes binaries directly โ no shell, no profile, no PATH expansion from .bashrc.
Solution 4: Install the missing command firstWhen the command genuinely isn't on the host, add the install step before the task that needs it:
- name: Ensure required tool is installed
package:
name: some-package
state: present
become: true
- name: Now run it
command: some_command --arg value
For pip-based tools:
- name: Install Python tool
pip:
name: some-tool
state: present
executable: pip3
- name: Use the installed tool
command: some-tool --version
Solution 5: Pin exact paths for version managers (nvm, pyenv, rbenv)Version managers install binaries into user-specific directories like ~/.nvm/versions/node/v20.11.0/bin/. Hardcode the full version path โ it's the only approach that works reliably:
- name: Run node via pinned nvm path
command: /home/deploy/.nvm/versions/node/v20.11.0/bin/node -e "console.log('ok')"
become: true
become_user: deploy
Store the version path in a playbook variable at the top, not scattered across tasks. When you upgrade Node from v20 to v22, you change one line instead of ten.
VerificationRun with verbose output to confirm the fix holds:
ansible-playbook your_playbook.yml --tags your_task_tag -v
Check for rc: 0 and no FAILED entries. If you want an explicit sanity check before the real task runs, add a quick probe:
- name: Confirm PATH is correct
command: which some_command
register: check
- debug:
msg: "Command found at: {{ check.stdout }}"
- name: Run the actual command
command: "{{ check.stdout }} --arg value"
The Sudo / become VariantA related failure catches a lot of people off guard. The command exists for your deploy user but not for root โ or vice versa:
fatal: [host]: FAILED! => {
'msg': '[Errno 2] No such file or directory: b\'some_command\'',
'rc': 2
}
With become: true, the task runs as root. Root typically has a shorter, more restricted PATH than a regular user. Pass PATH explicitly alongside become:
- name: Run as root with full PATH
command: some_command
become: true
environment:
PATH: "/usr/local/bin:/usr/bin:/bin:/sbin:/usr/sbin"

