This skill should be used when writing idempotent Ansible tasks, using command or shell modules, implementing changed_when and failed_when directives, creating check-before-create patterns, or troubleshooting tasks that always show "changed".
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
scripts/check_idempotency.pyTechniques for ensuring Ansible tasks are truly idempotent - producing the same result whether run once or multiple times.
Controls when Ansible reports a task as "changed". Critical for command and shell modules
which always report changed by default.
- name: Check if service exists
ansible.builtin.command: systemctl status myservice
register: service_check
changed_when: false # Read-only operation, never changes anything
Controls when Ansible considers a task failed. Allows graceful handling of expected errors.
- name: Check resource existence
ansible.builtin.command: check-resource {{ resource_id }}
register: check_result
failed_when: false # Don't fail, we'll check the result ourselves
Captures task output for use in changed_when and failed_when expressions.
- name: Run command
ansible.builtin.command: some-command
register: cmd_result
# Now cmd_result.rc, cmd_result.stdout, cmd_result.stderr are available
Make commands report "changed" only when something actually changed:
- name: Create Proxmox API token
ansible.builtin.command: >
pveum user token add {{ username }}@pam {{ token_name }}
register: token_result
changed_when: "'already exists' not in token_result.stderr"
failed_when:
- token_result.rc != 0
- "'already exists' not in token_result.stderr"
no_log: true
Key pattern: Detect specific output that indicates no change occurred.
Check if a resource exists before creating it:
- name: Check if VM template exists
ansible.builtin.shell: |
set -o pipefail
qm list | awk '{print $1}' | grep -q "^{{ template_id }}$"
args:
executable: /bin/bash
register: template_exists
changed_when: false # Checking doesn't change anything
failed_when: false # Not finding it isn't a failure
- name: Create VM template
ansible.builtin.command: >
qm create {{ template_id }}
--name {{ template_name }}
--memory 2048
when: template_exists.rc != 0 # Only create if doesn't exist
register: create_result
changed_when: create_result.rc == 0
Confirm resource creation succeeded:
- name: Create VM
ansible.builtin.command: >
qm create {{ vmid }} --name {{ vm_name }}
register: create_result
changed_when: true
- name: Verify VM was created
ansible.builtin.shell: |
set -o pipefail
qm list | grep "{{ vmid }}"
args:
executable: /bin/bash
register: verify_result
changed_when: false
failed_when: verify_result.rc != 0
Use output content to determine if change occurred:
- name: Update cluster configuration
ansible.builtin.command: update-config --apply
register: update_result
changed_when: "'Configuration updated' in update_result.stdout"
failed_when: "'Error' in update_result.stderr"
| Output Indicator | changed_when Expression |
|---|---|
| "already exists" | "'already exists' not in result.stderr" |
| "no changes" | "'no changes' not in result.stdout" |
| "created" | "'created' in result.stdout" |
| "updated" | "'updated' in result.stdout" |
| Exit code 0 = created | result.rc == 0 |
Allow specific "failures" that are actually expected:
- name: Run database migration
ansible.builtin.command: /usr/bin/migrate-database
register: migrate_result
failed_when:
- migrate_result.rc != 0
- "'already applied' not in migrate_result.stdout"
- "'no pending migrations' not in migrate_result.stdout"
changed_when: "'applied' in migrate_result.stdout and 'already' not in migrate_result.stdout"
Mark read-only operations as never changed:
# Checking status
- name: Get cluster status
ansible.builtin.command: pvecm status
register: cluster_status
changed_when: false
failed_when: false
# Gathering information
- name: List available images
ansible.builtin.command: qm list
register: vm_list
changed_when: false
# Verification checks
- name: Verify service is running
ansible.builtin.command: systemctl is-active nginx
register: nginx_status
changed_when: false
failed_when: false
Use until for operations that may need retries:
- name: Wait for service to be ready
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
register: health_check
until: health_check.status == 200
retries: 30
delay: 10
# Total wait: up to 5 minutes
With command:
- name: Wait for VM to get IP address
ansible.builtin.command: qm agent {{ vmid }} network-get-interfaces
register: vm_network
until: vm_network.rc == 0
retries: 12
delay: 5
changed_when: false
Use facts to track state across tasks:
- name: Check existing cluster status
ansible.builtin.command: pvecm status
register: cluster_status
failed_when: false
changed_when: false
- name: Set cluster facts
ansible.builtin.set_fact:
is_cluster_member: "{{ cluster_status.rc == 0 }}"
in_target_cluster: "{{ cluster_name in cluster_status.stdout }}"
- name: Create cluster
ansible.builtin.command: pvecm create {{ cluster_name }}
when: not in_target_cluster
register: cluster_create
changed_when: cluster_create.rc == 0
# BAD - Always shows changed
- name: Check status
ansible.builtin.command: systemctl status app
# GOOD
- name: Check status
ansible.builtin.command: systemctl status app
register: status_check
changed_when: false
failed_when: false
# BAD - Hides all errors
- name: Critical operation
ansible.builtin.command: important-command
failed_when: false
# GOOD - Only allow expected "errors"
- name: Critical operation
ansible.builtin.command: important-command
register: result
failed_when:
- result.rc != 0
- "'expected condition' not in result.stderr"
# BAD - Can't check results
- name: Run command
ansible.builtin.command: create-resource
# GOOD
- name: Run command
ansible.builtin.command: create-resource
register: result
changed_when: "'created' in result.stdout"
Use strict error handling in shell scripts:
- name: Run pipeline
ansible.builtin.shell: |
set -euo pipefail
cat data.txt | grep pattern | sort | uniq
args:
executable: /bin/bash
register: pipeline_result
changed_when: false
| Flag | Purpose |
|---|---|
-e | Exit on any command failure |
-u | Error on undefined variables |
-o pipefail | Catch errors in pipelines |
Verify playbooks are idempotent by running twice:
# First run - may show changes
uv run ansible-playbook playbooks/setup.yml
# Second run - should show 0 changes
uv run ansible-playbook playbooks/setup.yml
# If second run shows changes, playbook is NOT idempotent
# Never changed (read-only)
changed_when: false
# Always changed (one-time operations)
changed_when: true
# Based on output content
changed_when: "'created' in result.stdout"
changed_when: "'already exists' not in result.stderr"
changed_when: "'updated' in result.stdout"
# Based on return code
changed_when: result.rc == 0
changed_when: result.rc != 1
# Complex conditions
changed_when:
- result.rc == 0
- "'no changes' not in result.stdout"
Use the idempotency checker to analyze playbooks for common issues:
# Check a single playbook
${CLAUDE_PLUGIN_ROOT}/skills/ansible-idempotency/scripts/check_idempotency.py ansible/playbooks/my-playbook.yml
# Check multiple playbooks
${CLAUDE_PLUGIN_ROOT}/skills/ansible-idempotency/scripts/check_idempotency.py ansible/playbooks/*.yml
# Strict mode (info issues become warnings)
${CLAUDE_PLUGIN_ROOT}/skills/ansible-idempotency/scripts/check_idempotency.py --strict ansible/playbooks/my-playbook.yml
# Summary only
${CLAUDE_PLUGIN_ROOT}/skills/ansible-idempotency/scripts/check_idempotency.py --summary ansible/playbooks/*.yml
The script detects:
changed_whenset -euo pipefailno_log that may contain secretsScript location: ${CLAUDE_PLUGIN_ROOT}/skills/ansible-idempotency/scripts/check_idempotency.py