How to Run Remote Commands with Ansible Shell Module

How to Run Remote Commands with Ansible Shell Module
Published on Mar 16, 2022 Updated on Jul 10, 2026

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/sh shell 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:

Command Line
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:

Command Line
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:

Command Line
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:

Command Line
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.

Command Line
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:

Command Line
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:

Command Line
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.

Command Line
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:

Command Line
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`.


Bare Metal Servers - 12 Minute Deployment

Get 100% dedicated resources for high-performance workloads.

Share this article

Related Articles

Published on Jul 21, 2025 Updated on Nov 7, 2025

Ansible Cheat Sheet: Ansible Command Guide

Master Ansible with essential commands, playbook tips, privilege escalation, debugging, and best practices to automate infrastructure agentlessly.

Read More
Published on Mar 20, 2025 Updated on Jun 25, 2026

How to Install Ansible on Ubuntu 24.04: Step-by-Step Guide

Learn how to install Ansible on Ubuntu 24.04 using both package manager and pip. Set up an inventory, run playbooks, and automate IT tasks efficiently.

Read More
Published on Jan 16, 2025 Updated on Nov 7, 2025

How to Use Ansible Vault in Playbook? Secure Your Configuration

Learn to secure sensitive data in Ansible Playbooks using Ansible Vault. Encrypt files, variables, and manage secrets with step-by-step guidance and best practices.

Read More
No results found for ""
Recent Searches
Navigate
Go
ESC
Exit