An Ansible ad hoc command runs one module against a host pattern from the CLI—no playbook file required. You use it to test SSH, check uptime, install a package, or restart a service before you commit the same task to YAML.
This guide walks through syntax, the modules you reach for daily, privilege escalation, host targeting, and how to read Ansible output. I run commands from ~/ansible-project on rocky1 against rocky2 in the EX294 lab. You need inventory basics and the project layout first.
Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16.
~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
What are Ansible Ad Hoc Commands?
An ad hoc command is ansible plus a host pattern, a module (-m), and optional module arguments (-a). Ansible connects over SSH, runs the module once per matched host, and prints JSON (or a one-line summary) for each result.
Playbooks chain many tasks with variables, handlers, and roles. Ad hoc commands are the fast path: one task, one command line, immediate feedback.
ansible <pattern> -m <module> -a "<module arguments>"The default module is command, so ansible lab -a "uptime" runs the command module even when you omit -m.
When Should You Use Ad Hoc Commands?
Reach for ad hoc commands when you need speed or a sanity check:
- Verify SSH and Python after lab setup (
ping) - Inspect one host before a change (
uptime,df,systemctl status) - Apply a one-time fix on a small set of machines
- Learn what a module does before you embed it in a playbook
Move to playbooks and the playbook structure guide when the same steps repeat, multiple tasks must run in order, you need idempotent roles, or teammates must review changes in Git. Ad hoc output is not a substitute for version-controlled automation.
Basic Ansible Ad Hoc Command Syntax
Every ad hoc command has the same skeleton:
| Part | Flag / position | Example |
|---|---|---|
| Host pattern | First argument | lab, rocky2, all |
| Module | -m |
-m ping |
| Module args | -a |
-a "name=tmux state=present" |
| Inventory | -i (if not in ansible.cfg) |
-i inventory/hosts |
| Remote user | -u |
-u ansible |
| Privilege escalation | -b / --become |
-b |
| Limit subset | --limit |
--limit rocky2 |
| Dry run | -C / --check |
-C |
| Verbosity | -v, -vv, -vvv |
-v |
List matched hosts without running a module:
ansible lab --list-hostsSample output:
hosts (1):
rocky2Find Module Options with ansible-doc
Before running an unfamiliar module, check its options with ansible-doc—see modules, ansible-doc and collections for FQCN naming, return values, and collection install paths.
ansible-doc ansible.builtin.dnfSample output:
> ANSIBLE.BUILTIN.DNF (/usr/lib/python3.12/site-packages/ansible/modules/dnf.py)
Installs, upgrade, removes, and lists packages and groups with
the `dnf' package manager.Search for modules by keyword:
ansible-doc -l | grep -i serviceSample output:
ansible.builtin.service Manage services
ansible.builtin.systemd_service Manage systemd unitsShow the full option list and examples for one module:
ansible-doc ansible.builtin.copyUse ansible-doc when you are not sure which arguments a module accepts—it is safer than guessing -a values from memory.
Test Connectivity with ping Module
Start with ping. It confirms Ansible can connect and run Python on the managed node—it is not ICMP.
ansible lab -m pingSample output:
rocky2 | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": false,
"ping": "pong"
}pong means SSH and the module pipeline work. If this fails, fix inventory, keys, or ansible_user before trying package or service modules.
Target one host by inventory name:
ansible rocky2 -m pingGather Facts with setup Module
Use setup to inspect facts Ansible collects from a managed node—useful before writing playbook conditions. The facts and custom facts guide covers gather_facts, filters, and ansible_local in depth.
ansible rocky2 -m setup -a "filter=ansible_distribution*"Sample output:
rocky2 | SUCCESS => {
"ansible_facts": {
"ansible_distribution": "Rocky",
"ansible_distribution_major_version": "10",
"ansible_distribution_version": "10.2"
},
"changed": false
}Omit the filter to dump all facts—verbose on a large host. Use setup for quick OS and network checks; move repeated fact gathering into playbooks.
Run Commands with command Module
The command module runs a program directly—no shell features like pipes, redirects, globbing, chaining, or shell builtins. Use shell only when you need shell syntax.
ansible lab -m command -a "uptime"Sample output:
rocky2 | CHANGED | rc=0 >>
15:38:25 up 17:41, 1 user, load average: 0.25, 0.06, 0.02CHANGED here only means the command ran successfully; command does not track configuration state the way package or file do.
Run Shell Commands with shell Module
Use shell when you need shell syntax—pipes, &&, or variable expansion.
ansible lab -m shell -a 'echo $SHELL && hostname -s'Sample output:
rocky2 | CHANGED | rc=0 >>
/bin/bash
rocky2Prefer command when a plain executable is enough. Reserve shell for pipelines and shell builtins.
command vs shell Module
| Feature | command |
shell |
|---|---|---|
Runs through /bin/sh |
No | Yes |
Pipes (|) |
No | Yes |
Redirects (>, >>) |
No | Yes |
| Shell metacharacters | No | Yes |
| Shell builtins / chaining | No | Yes |
| Safer default | Yes | Use only when needed |
Some simple variable expansion may work with command, but do not rely on shell behavior there. Use shell only when you need shell syntax such as pipes, redirects, &&, or shell builtins.
Official guidance: use command unless shell metacharacters like <, >, or | are required—parsing them through a shell can create security surprises. Treat shell -a like user input. For a deeper comparison including raw, argv, and idempotency patterns, see command vs shell vs raw.
Make command and shell Safer with creates or removes
command and shell usually report CHANGED whenever they run. Add creates or removes when a command should run only if a file does not exist or already exists.
Run only when the marker file does not already exist:
ansible lab -m command -a "touch /tmp/ad-hoc-marker creates=/tmp/ad-hoc-marker" -bSample output:
rocky2 | CHANGED | rc=0 >>Run the same command again and Ansible skips it:
rocky2 | SUCCESS | rc=0 >>
skipped, since /tmp/ad-hoc-marker existsRun only when the file exists:
ansible lab -m command -a "rm /tmp/ad-hoc-marker removes=/tmp/ad-hoc-marker" -bSample output:
rocky2 | CHANGED | rc=0 >>This does not make arbitrary shell workflows fully idempotent, but it prevents repeated runs for simple file-based commands.
Manage Packages with package, dnf and apt Modules
ansible.cfg enables become = True, so examples below work without extra flags. The commands include -b anyway so you can copy them into projects that do not set become by default.
package (cross-platform)
package picks the OS package manager for you—handy in mixed inventories.
ansible lab -m package -a "name=tmux state=present" -bSample output:
rocky2 | CHANGED => {
"changed": true,
"msg": "",
"rc": 0,
"results": [
"Installed: tmux-3.3a-13.20250207gitb202a2f.el10.x86_64"
]
}changed: true means Ansible installed a package. A second run with state=present reports changed: false when the package is already there.
dnf (Rocky Linux / RHEL)
On Rocky Linux 10, dnf is explicit and matches what you would run locally:
ansible lab -m dnf -a "name=tmux state=present" -bSample output:
rocky2 | SUCCESS => {
"changed": false,
"msg": "Nothing to do",
"rc": 0,
"results": []
}apt (Debian / Ubuntu)
On Debian-family hosts, the same idea uses apt:
ansible web -m apt -a "name=git state=present" -bUse state=absent to remove a package. Package modules need privilege escalation on most systems—add -b unless ansible.cfg already sets become.
Manage Services with service and systemd_service Modules
service
service works across init systems; on Rocky Linux 10 it drives systemd units.
ansible lab -m service -a "name=sshd state=started" -bSample output:
rocky2 | SUCCESS => {
"changed": false,
"name": "sshd",
"state": "started"
}Other useful state values: stopped, restarted, reloaded.
systemd_service
systemd_service exposes more systemd-specific options (enabled, daemon_reload, unit drop-ins). For a simple start check, output matches service:
ansible lab -m systemd_service -a "name=sshd state=started" -bSample output:
rocky2 | SUCCESS => {
"changed": false,
"name": "sshd",
"state": "started"
}Use systemd_service when you need unit-file details; use service for portable start/stop/restart tasks.
Manage Users and Groups
Create a local user:
ansible lab -m user -a "name=demo_ansible state=present shell=/bin/bash" -bSample output:
rocky2 | CHANGED => {
"changed": true,
"group": 1003,
"home": "/home/demo_ansible",
"name": "demo_ansible",
"shell": "/bin/bash",
"state": "present",
"uid": 1002
}Create a group:
ansible lab -m group -a "name=demo_grp state=present" -bSample output:
rocky2 | CHANGED => {
"changed": true,
"gid": 1002,
"name": "demo_grp",
"state": "present"
}Use state=absent to remove users or groups. Set passwords through playbooks and Vault—avoid plain-text password hashes on the command line in shared history.
Manage Files and Directories
The file module creates directories, sets permissions, and removes paths.
ansible lab -m file -a "path=/tmp/ansible-demo state=directory mode=0755" -bSample output:
rocky2 | CHANGED => {
"changed": true,
"gid": 0,
"mode": "0755",
"owner": "root",
"path": "/tmp/ansible-demo",
"state": "directory"
}Run the same command again and Ansible reports changed: false when the directory already matches.
ansible lab -m file -a "path=/tmp/ansible-demo state=directory mode=0755" -bSample output:
rocky2 | SUCCESS => {
"changed": false,
"path": "/tmp/ansible-demo",
"state": "directory"
}Use state=absent to delete a file or directory, state=touch for an empty file, or state=link for symlinks.
Copy Files to Managed Nodes
The copy module pushes a file from the control node to managed hosts.
On rocky1, create a small source file:
echo "# demo marker" > /tmp/demo-copy.txtCopy it to rocky2:
ansible lab -m copy -a "src=/tmp/demo-copy.txt dest=/tmp/ansible-demo/marker.txt mode=0644" -bSample output:
rocky2 | CHANGED => {
"changed": true,
"checksum": "13fc6d78c1029585c0b3980b74cca1318148e17b",
"dest": "/tmp/ansible-demo/marker.txt",
"mode": "0644",
"size": 14
}src must exist on the control node. For content generated from templates, use playbooks and the template module.
Use Privilege Escalation with become
Many modules need root. Pass -b or --become, or set become = True in ansible.cfg as this lab does.
Run whoami as root:
ansible lab -m command -a "whoami" -bSample output:
rocky2 | CHANGED | rc=0 >>
rootDisable become for one command when your project defaults it on:
ansible lab -m command -a "whoami" -e ansible_become=falseSample output:
rocky2 | CHANGED | rc=0 >>
ansible-K / --ask-become-pass prompts for the sudo password when NOPASSWD is not configured. In the EX294 lab, passwordless sudo for ansible is already set up on rocky2.
Target Hosts and Groups in Ad Hoc Commands
The first argument is an inventory pattern. Examples:
| Goal | Command |
|---|---|
| All hosts | ansible all -m ping |
| One group | ansible lab -m ping |
| One host | ansible rocky2 -m ping |
| Multiple groups (OR) | ansible web:db -m ping |
| Intersection (AND) | ansible 'web:&prod' -m ping |
| Exclude hosts | ansible 'all:!db' -m ping |
Always confirm the matched set before destructive modules:
ansible lab --list-hostsSee the patterns guide for wildcards, regex (~pattern), and safe --limit usage.
Use --limit with Ad Hoc Commands
--limit narrows the pattern at runtime without editing inventory:
ansible all --limit rocky2 -m pingList hosts that would run:
ansible all --limit rocky2 --list-hostsSample output:
hosts (1):
rocky2--limit intersects with the pattern. ansible lab --limit rocky2 and ansible all --limit rocky2 both hit rocky2 when it is in inventory. Quote patterns that contain ! so the shell does not interpret them.
Use Check Mode with Ad Hoc Commands
Check mode (-C / --check) tries to show what would change without applying the change:
ansible lab -m dnf -a "name=tree state=present" -b -CSample output:
rocky2 | CHANGED => {
"changed": true,
"msg": "Check mode: No changes made, but would have if not in check mode",
"results": [
"Installed: tree-2.1.0-8.el10.x86_64"
]
}Useful before package, file, user, and service changes. Not every module can predict changes in check mode—modules such as command and shell are poor dry-run tools because Ansible cannot know what an arbitrary command would alter.
For file-related modules, add --diff when supported:
ansible lab -m copy -a "src=/tmp/demo-copy.txt dest=/tmp/ansible-demo/marker.txt" -b --diff -CSample output:
--- before
+++ after: /tmp/demo-copy.txt
@@ -0,0 +1 @@
+# demo marker
rocky2 | CHANGED => {
"changed": true
}Treat check mode as a safety check, not a guarantee.
Check Command Output and Changed Status
Ansible prints a status color and JSON per host:
| Line prefix | Meaning |
|---|---|
SUCCESS |
Module finished; often changed: false |
CHANGED |
Module altered the system (or command/shell ran) |
FAILED |
Non-zero rc or module error |
UNREACHABLE |
SSH or connection failure |
Read changed for idempotent modules (package, file, user). command and shell usually show CHANGED whenever they run, even if they only print text.
Add -v for more detail:
ansible lab -m command -a "uname -r" -vSample output:
Using /home/ansible/ansible-project/ansible.cfg as config file
rocky2 | CHANGED | rc=0 >>
6.12.0-211.16.1.el10_2.0.1.x86_64Use -vvv when debugging connection or become issues.
Common Ad Hoc Command Examples
| Task | Command |
|---|---|
| Ping all lab hosts | ansible lab -m ping |
| Check uptime | ansible lab -m command -a "uptime" |
| Install package (Rocky) | ansible lab -m dnf -a "name=tmux state=present" -b |
| Ensure service running | ansible lab -m service -a "name=sshd state=started" -b |
| Create directory | ansible lab -m file -a "path=/opt/app state=directory mode=0755" -b |
| Copy file to host | ansible lab -m copy -a "src=./files/app.conf dest=/etc/app/app.conf" -b |
| Add user | ansible lab -m user -a "name=deploy state=present" -b |
| Run on one host | ansible rocky2 -m ping |
| Dry run | ansible lab -m dnf -a "name=httpd state=present" -b -C |
| List targets | ansible lab --list-hosts |
| Gather facts | ansible rocky2 -m setup -a "filter=ansible_distribution*" |
| Check module docs | ansible-doc ansible.builtin.dnf |
Other Useful Ad Hoc Modules
| Module | Example use |
|---|---|
setup |
Gather facts from a host |
stat |
Check whether a file exists without changing it |
lineinfile |
Edit one line in a file |
fetch |
Copy a file from managed node to control node |
reboot |
Reboot a host safely |
Check a file without modifying it:
ansible lab -m stat -a "path=/etc/ssh/sshd_config" -bSample output:
rocky2 | SUCCESS => {
"changed": false,
"stat": {
"exists": true,
"path": "/etc/ssh/sshd_config",
"mode": "0600",
"size": 3674
}
}Use these for quick checks or emergencies. For repeated config changes—especially lineinfile—convert the task to a playbook so the edit is version-controlled.
Troubleshoot Common Ad Hoc Command Errors
| Error / symptom | Meaning | What to check |
|---|---|---|
No hosts matched |
Pattern or inventory path is wrong | Run ansible --version, check config file, then ansible all --list-hosts |
UNREACHABLE |
SSH connection failed | Check ansible_host, SSH key, username, firewall, and host-only IP |
FAILED! => ... Permission denied |
Module needed root | Add -b or configure become |
Missing sudo password |
sudo requires a password | Use -K or configure passwordless sudo in lab |
MODULE FAILURE |
Module ran but failed on target | Add -vvv, check Python, package name, or module args |
Could not find or access src |
Local file path is wrong | Remember copy src= is on the control node by default |
No package matching ... |
Package name is wrong for that distro | Try dnf search on the target or use distro-specific names |
Start with connectivity:
ansible all --list-hostsansible rocky2 -m ping -vvvThen test privilege escalation:
ansible rocky2 -m command -a "whoami" -bCommon Mistakes with Ansible Ad Hoc Commands
| Mistake | Result | Fix |
|---|---|---|
Using shell when command is enough |
Unnecessary shell injection risk | Prefer command |
Quoting -a wrong in the shell |
Module args parsed incorrectly | Use double quotes outside, single inside: -a 'echo $SHELL' |
Forgetting -b for package/file tasks |
Permission denied on managed node | Add -b or set become in ansible.cfg |
| Wrong inventory path | No hosts matched |
Run from project root or pass -i inventory/hosts |
Expecting ICMP from ping |
Confusion when firewall blocks ICMP | ping module tests Ansible connectivity |
Piping in command |
Module fails or splits args wrong | Use shell or pass one executable |
| Secrets on the CLI | Passwords in shell history | Use playbooks, Vault, or --ask-pass |
No --list-hosts before destructive change |
Wrong machines targeted | ansible PATTERN --list-hosts first |
| Re-running complex workflows ad hoc | Untracked, unreproducible changes | Convert to a playbook |
When to Move from Ad Hoc Commands to Playbooks
Stay on ad hoc while exploring. Switch to playbooks when you notice:
- The same
-m/-aline in your shell history more than twice - Multiple modules that must run in order (install, configure, restart)
- Need for handlers, conditionals, or roles
- Team review, CI, or scheduled runs
- Variables from
group_varsthat belong with the task definition
A practical path: prove the module ad hoc, then paste the working task into playbooks/site.yml and use group_vars and patterns for targeting and variables.
What comes next
- Ansible playbook examples — multi-task YAML workflows
- group_vars, host_vars and inventory patterns — variables and targeting
- ansible.cfg — defaults for inventory, become, and SSH
References
- Introduction to ad hoc commands
- ansible command-line tool
- ansible-doc command
- Patterns: selecting hosts and groups
Summary
Ansible ad hoc commands run one module against an inventory pattern: ansible <pattern> -m <module> -a "<args>". Use ping and setup to test hosts and gather facts, command or shell to run programs, and package, dnf, service, file, copy, user, and group for common administration. Add -b when tasks need root, confirm targets with --list-hosts, use -C for dry runs where modules support it, and read changed plus FAILED/UNREACHABLE in the output. Check unfamiliar modules with ansible-doc. When tasks repeat or grow, move them into playbooks.

