How to Run Remote Commands with Ansible Shell Module
Most server tasks come down to running a command on a remote host. The Ansible shell module does exactly that. It executes commands on managed nodes through their shell, with full support for pipes, redirection, and environment variables. The module ships with ansible-core, so every Ansible installation already includes it.
You will start with ad-hoc commands, then move into playbooks that handle conditional execution, working directories, environment variables, and chained commands. By the end, you will know when to reach for the shell module, when a purpose-built module fits the job better, and how to run shell tasks without exposing your hosts to injection.
#What is the Ansible shell module?
The Ansible shell module is a handy module that allows you to directly execute commands on the shell of remote targets, just as you would if you were logged in. By doing so, it helps maintain the originality of command execution.
The shell module is closely related to the command module. Both help achieve the same result. However, a few differences exist between the two:
-
The shell module executes commands directly on the shell of target hosts. By default, the shell module uses the
/bin/shshell to run commands, although you can configure it to use other shells. With the command module, the executed commands are not processed through the shell. -
Since commands are not executed on the shell, the command module does not support environment variables, pipes and other operators such as '>' , '<' , '&', ';' and '| |'. With shell module, piping, redirection and variables are fully supported. Thus, the shell module provides more flexibility.
-
If running commands securely on target systems is your priority, use the command module. Unlike the shell module, the command module is not affected by the remote user's shell environment.
#Ansible shell module vs other modules
The Ansible shell module falls in the same category as the command, script, and raw modules. These are collectively referred to as run commands.
While efficient in getting things done fairly fast, run commands should only be used as a last resort. This is because they apply the least logic while executing tasks and the concept of the desired state is non-existent. Subsequent execution of the shell command might fail if a condition has already been met leading to errors.
In addition, catching errors is not possible with the shell module unless you register the result of the first command. You will then have to follow it up with a subsequent task in the playbook to implement a conditional logic to confirm if the error occurred and then deal with it. This can result in bottlenecks that considerably undermine automation.
For this reason, shell commands should be limited to carrying out simple tasks on remote systems. Where the desired state of services or applications is required, task-specific modules such as service, copy, file, and lineinfile, to name a few, are recommended. This makes playbooks more versatile and reusable.
The four run-command modules differ in how they reach the target and which shell features they allow. The table below summarizes where each one fits before you commit to the shell module.
| Module | Runs through a shell | Pipes, redirection, and variables | Best suited for |
|---|---|---|---|
ansible.builtin.shell |
Yes, /bin/sh by default |
Supported | Commands that depend on shell features |
ansible.builtin.command |
No | Not supported | Simple command execution with less risk |
ansible.builtin.raw |
Yes, with no module subsystem | Supported | Bootstrapping a host that has no Python yet |
ansible.builtin.script |
Yes, runs a local script on the target | Supported inside the script | Running an existing script across hosts |
Run your deployments in a scalable and cost-effective open cloud infrastructure. Cherry Servers' secure virtual private servers offer automatic scaling, flexible pricing, and 24/7 technical support. Our pre-defined Ansible module helps you automate the installation and configuration.
#Prerequisites
To follow this guide, you should have:
-
An Ansible control node. This tutorial uses Ubuntu 24.04 with ansible-core 2.20, though the steps work on most modern Linux distributions. If you have not installed Ansible yet, follow our guide on how to install Ansible on Ubuntu 24.04.
-
A remote host against which you will run the commands.
#Run Ansible ad-hoc shell commands
The true power of Ansible lies in playbooks. However, playbooks are used to execute elaborate tasks on target hosts. Sometimes you want to run a command quickly without saving it for later use. Ad-hoc commands handle exactly that.
Ad-hoc commands are one-liner commands that you can run on the fly without the need for writing a playbook.
For instance, you might want to check the uptime or disk space utilization of your remote hosts. Instead of writing an entire playbook for such tasks, a better approach would be to run ad-hoc commands against your hosts.
Ad-hoc commands take the following syntax:
ansible [pattern] -m [MODULE] -a {COMMAND_OPTIONS}
The pattern specifies the host group that the target host belongs to. The -m option specifies the type of module while the -a option takes the command arguments.
The module name shell is shorthand for its fully qualified collection name (FQCN), ansible.builtin.shell. The ansible.builtin prefix marks it as part of ansible-core. Ad-hoc commands accept the short name, while playbooks should use the FQCN to avoid clashes with modules from other collections.
Let us take a few examples.
To check the uptime of all the target hosts, run the command:
sudo ansible all -m shell -a 'uptime -p'
Outputweb01 | CHANGED | rc=0 >>
up 3 weeks, 2 days, 7 hours, 19 minutes
web02 | CHANGED | rc=0 >>
up 5 days, 14 hours, 3 minutes
To check the memory usage, run:
sudo ansible all -m shell -a 'free -m'
Outputweb01 | CHANGED | rc=0 >>
total used free shared buff/cache available
Mem: 3936 512 2871 1 552 3187
Swap: 0 0 0
To check disk space utilization of the host under the db_server group, execute:
sudo ansible db_server -m shell -a 'df -Th'
Outputdb01 | CHANGED | rc=0 >>
Filesystem Type Size Used Avail Use% Mounted on
/dev/vda1 ext4 49G 8.2G 39G 18% /
tmpfs tmpfs 2.0G 0 2.0G 0% /dev/shm
/dev/vda15 vfat 105M 6.1M 99M 6% /boot/efi
#Run a single command with Ansible shell module
Aside from running ad hoc commands, the Ansible shell module is also used in playbooks to specify the tasks to be carried out on remote hosts.
Consider the playbook below.
---
- name: Shell module example
hosts: webservers
tasks:
- name: Check system information
ansible.builtin.shell: lsb_release -a
register: os_info
- name: Show the OS information
ansible.builtin.debug:
msg: ""
In this example, the playbook runs against a host group called webservers and executes an lsb_release -a command which retrieves details about the OS version. The output is then saved in a register variable called os_info.
The last line prints out the output stored in the os_info variable to stdout using the debug module.
Here is the output of the playbook execution.
OutputPLAY [Shell module example] ****************************************************
TASK [Gathering Facts] *********************************************************
ok: [web01]
TASK [Check system information] ************************************************
changed: [web01]
TASK [Show the OS information] *************************************************
ok: [web01] => {
"msg": [
"No LSB modules are available.",
"Distributor ID:\tUbuntu",
"Description:\tUbuntu 24.04.2 LTS",
"Release:\t24.04",
"Codename:\tnoble"
]
}
PLAY RECAP *********************************************************************
web01 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
#Key parameters of the Ansible shell module
The shell module accepts a small set of arguments that control when and where a command runs. You will use several of them in the examples that follow, so the table below collects them in one place for reference.
| Parameter | What it does |
|---|---|
| Free-form command | The command string to run, passed directly after the module name |
creates |
Skips the task when the named file already exists |
removes |
Runs the task only when the named file exists |
chdir |
Changes into a directory before running the command |
executable |
Runs the command with a specific shell instead of /bin/sh |
stdin |
Passes a string to the command on standard input |
#Run a command using shell module if a file does not exist
The creates parameter allows you to run a command if a file does not exist. It specifies the path to the file; if it exists, the command to be executed is skipped.
The playbook shown checks if the file hello.txt exists in the home directory of the target host. If the file is absent, then the shell command specified is executed. If the file exists, then the shell command aborts.
---
- name: Create a file in the home directory if it does not exist
hosts: webservers
tasks:
- name: Create a file in the home directory
ansible.builtin.shell: echo "Hey guys!" > $HOME/hello.txt
args:
creates: "$HOME/hello.txt"
Since the file does not exist on the remote target, the shell command is successfully executed, as you can see from the playbook execution.
OutputPLAY [Create a file in the home directory if it does not exist] ***************
TASK [Gathering Facts] *********************************************************
ok: [web01]
TASK [Create a file in the home directory] ************************************
changed: [web01]
PLAY RECAP *********************************************************************
web01 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The command below confirms that the hello.txt file was created in the remote target's home directory.
ssh root@173.82.120.115 "ls -l ~"
Outputtotal 4
-rw-r--r-- 1 root root 11 Jun 18 09:42 hello.txt
#Run a command using shell module if a file exists
The removes parameter specifies the filename, and if the file exists, the command is executed. In the previous example, the hello.txt file was created in the remote target's home directory.
In this playbook, the removes parameter checks if this file exists on the remote target. And since you already created it, the playbook proceeds to execute the shell command, which removes the file.
---
- name: Remove a file in the home directory only if it exists
hosts: webservers
tasks:
- name: Remove a file in the home directory
ansible.builtin.shell: rm $HOME/hello.txt
args:
removes: "$HOME/hello.txt"
Earlier versions of this playbook included a warn: false argument. The warn parameter was removed in ansible-core 2.14, so passing it now results in the task failing. Current versions of the shell and command modules no longer emit those warnings, so the argument is unnecessary.
The playbook execution confirms that the file was removed.
OutputPLAY [Remove a file in the home directory only if it exists] ******************
TASK [Gathering Facts] *********************************************************
ok: [web01]
TASK [Remove a file in the home directory] ************************************
changed: [web01]
PLAY RECAP *********************************************************************
web01 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
#Run a command in a different directory
To run a shell command inside a specific directory, use the chdir parameter. The playbook below downloads the Apache HTTP Server source tarball into the /usr/local/src directory.
---
- name: Download Apache source file to /usr/local/src directory
hosts: webservers
become: yes
tasks:
- name: Download Apache tarball file
ansible.builtin.shell: wget https://dlcdn.apache.org/httpd/httpd-2.4.68.tar.gz
args:
chdir: /usr/local/src
Writing into /usr/local/src needs root, so the playbook sets become: yes to run the task with elevated privileges. Run it as usual:
ansible-playbook download_apache.yml
If the task fails with a "permission denied" error while trying to write the file, privilege escalation is not completing, because the remote user's sudo account requires a password that Ansible has not been given. Add the -K option to be prompted for that sudo password:
ansible-playbook download_apache.yml -K
Ansible asks for the BECOME password on the shell. Enter the remote user's sudo password, and the task writes the tarball to /usr/local/src without errors. If your remote user has passwordless sudo, the plain command works as is and -K is not needed.
The download mirror keeps only the current release, so update the version number if a newer one has shipped. You can find the latest release on the official Apache HTTP Server download page.
The playbook confirms that the task was successfully carried out.
OutputPLAY [Download Apache source file to /usr/local/src directory] ****************
TASK [Gathering Facts] *********************************************************
ok: [web01]
TASK [Download Apache tarball file] ******************************************
changed: [web01]
PLAY RECAP *********************************************************************
web01 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
#Run a command under a different shell
By default, the shell module runs commands with /bin/sh, which on many systems is a minimal shell. When a command relies on features specific to Bash, such as brace expansion or process substitution, you can switch shells with the executable parameter.
The playbook below runs a Bash-specific command under /bin/bash.
---
- name: Run a command with a specific shell
hosts: webservers
tasks:
- name: Use a Bash feature through the executable parameter
ansible.builtin.shell: echo {1..5}
args:
executable: /bin/bash
register: brace_output
- name: Show the result
ansible.builtin.debug:
msg: ""
Brace expansion ({1..5}) is a Bash feature that /bin/sh does not expand. Setting executable to /bin/bash produces the expected result. The output block below uses the output attribute.
Outputok: [web01] => {
"msg": "1 2 3 4 5"
}
#Use Ansible shell module with environment variables
The shell module also enables you to set new environment variables. This is made possible using the environment parameter. Consider the playbook below.
---
- name: Environment variable example
hosts: webservers
tasks:
- name: Set an environment variable for the task
ansible.builtin.shell: echo $NEW_VAR
register: command_result
environment:
NEW_VAR: "john_doe"
- name: Show the command result
ansible.builtin.debug:
msg: ""
The playbook sets the NEW_VAR environment variable to john_doe.
NOTE: The environment variable is only set for that particular task. In subsequent tasks, the NEW_VAR variable will not be available.
OutputPLAY [Environment variable example] *******************************************
TASK [Gathering Facts] *********************************************************
ok: [web01]
TASK [Set an environment variable for the task] ******************************
changed: [web01]
TASK [Show the command result] ***********************************************
ok: [web01] => {
"msg": [
"john_doe"
]
}
PLAY RECAP *********************************************************************
web01 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
#Run multiple commands with Ansible shell module
So far, you have seen the shell module running single commands on managed hosts. You can also specify a set of commands to be carried in chronological order.
To achieve this, start off the shell command with a vertical bar, followed by a list of tasks to be carried out. In this example, the output of the date, uptime, and echo command is saved to the date.txt, uptime.txt and hello.txt files respectively which are then saved in the /tmp directory.
PLAY [Environment variable example] *******************************************
TASK [Gathering Facts] *********************************************************
ok: [web01]
TASK [Set an environment variable for the task] ******************************
changed: [web01]
TASK [Show the command result] ***********************************************
ok: [web01] => {
"msg": [
"john_doe"
]
}
PLAY RECAP *********************************************************************
web01 : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The playbook runs the tasks sequentially from the first task to the last.
OutputPLAY [Shell module example] ***************************************************
TASK [Gathering Facts] *******************************************************
ok: [web01]
TASK [Run multiple commands] ************************************************
changed: [web01]
PLAY RECAP *******************************************************************
web01 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
#Run commands with pipes and redirection
As previously mentioned, the shell module also accepts pipes and redirections. In fact, the previous playbook leveraged the redirection symbol ( > ) to save the output of the listed commands to separate files.
Suppose you want to list all the text files created in the /tmp directory and save the result to another file called dirlist.txt in the same directory.
Here is what the shell command would look like.
ls -l /tmp | grep .txt > /tmp/dirlist.txt
The first part of the command lists all the files in the /tmp directory. The result is then piped to the grep command which filters the output to include only the text files. The final output is then saved to the dirlist.txt file using the 'greater than' redirection symbol.
Now, let us go a step further and print the result to stdout. For that, a second task is required. The goal is to display the contents of the dirlist.txt file to stdout. The shell command for listing the file's content is:
cat /tmp/dirlist.txt
The output of the command is then registered in a variable called displayfile and the message is displayed to stdout using the debug module. Here is what the entire playbook looks like.
---
- name: Shell module example
hosts: webservers
tasks:
- name: List text files in tmp directory and save result in output file
ansible.builtin.shell: "ls -l /tmp | grep .txt > /tmp/dirlist.txt"
- name: Display the contents of the output file
ansible.builtin.shell: cat /tmp/dirlist.txt
register: displayfile
- name: Show the directory listing
ansible.builtin.debug:
msg: ""
When the playbook is executed, all the text files in the /tmp directory, including the dirlist.txt file, are printed to stdout:
OutputPLAY [Shell module example] ***************************************************
TASK [Gathering Facts] *******************************************************
ok: [web01]
TASK [List text files in tmp directory and save result in output file] ******
changed: [web01]
TASK [Display the contents of the output file] ******************************
changed: [web01]
TASK [Show the directory listing] *******************************************
ok: [web01] => {
"msg": [
"-rw-r--r-- 1 root root 29 Jun 18 09:50 date.txt",
"-rw-r--r-- 1 root root 0 Jun 18 09:50 dirlist.txt",
"-rw-r--r-- 1 root root 12 Jun 18 09:50 hello.txt",
"-rw-r--r-- 1 root root 64 Jun 18 09:50 uptime.txt"
]
}
PLAY RECAP *******************************************************************
web01 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
#Make shell tasks idempotent
Run commands carry a known weakness. They report changed on every run, because the shell module cannot tell whether a command altered anything on the host. That behavior violates the desired-state concept and makes playbook output noisy. Three techniques keep shell tasks predictable.
First, use creates and removes to tie a task to a file, as shown in the earlier examples. The task then runs only when there is work to do.
Second, set changed_when to define what counts as a change. A task that only reads information should never report changed.
- name: Check the running kernel
ansible.builtin.shell: uname -r
register: kernel
changed_when: false
Third, set failed_when to control what counts as a failure, since a non-zero exit code does not always mean something went wrong. Used together, these options bring shell tasks much closer to the behavior of purpose-built modules.
#Prevent shell injection
Since it runs commands on the shell on remote targets, the shell module is susceptible to shell command injections. Shell injection is an attack vector in which the attacker runs arbitrary commands on the host to compromise the underlying infrastructure.
When using Shell Module variables, Ansible recommends using the quote filter to prevent shell injection threats.
Consider a simple playbook below.
---
- name: Ansible quote filter demo
hosts: webservers
vars:
username: cherry
tasks:
- name: Print variable
ansible.builtin.debug:
msg: "Running playbook as user "
The username variable is referenced at the very end by the msg parameter using the quote filter to prevent an arbitrary command string from being executed if it were injected with the username variable.
Also read: How to use 'when' condition in Ansible
#Conclusion
The Ansible shell module gives you a direct way to run commands on managed nodes when no purpose-built module exists for the job. You have seen how to run ad-hoc commands, control execution with creates, removes, and chdir, switch shells with executable, set environment variables, chain commands, and keep tasks predictable with changed_when and failed_when.
For most configuration work, reach for task-specific modules first, since they track desired state and stay idempotent. When the shell module is the right tool, apply the quote filter to any templated variable to guard against injection.
To build on this, the Ansible cheat sheet offers a broader command reference, and the official shell module documentation lists every parameter the module accepts.
FAQs
What is the difference between the shell and command modules in Ansible?
The shell module runs commands through a shell, so it supports pipes, redirection, and environment variables. The command module runs commands directly without a shell, which is safer but does not support those features.
How do I run an Ansible shell command with sudo?
Add `become: true` to the task or the play. Ansible then runs the command with elevated privileges using the configured become method.
Can the Ansible shell module run multiple commands?
Yes. Use a block scalar with `shell: |` and list each command on its own line. The module runs them in order.
How do I capture the output of a shell command?
Add `register` to store the result in a variable, then read `stdout` or `stdout_lines` from that variable, usually with the debug module.
What does `ansible.builtin.shell` mean?
It is the fully qualified collection name for the shell module. The `ansible.builtin` prefix shows the module ships with ansible-core and avoids name clashes with modules from other collections.
Is the Ansible shell module idempotent?
Not by default, since it reports a change on every run. You can make tasks conditional with `creates`, `removes`, `changed_when`, and `failed_when`.
Get 100% dedicated resources for high-performance workloads.