Introduction

Ansible-lint rule 203 (no-tabs) flags tab characters (\t) in playbooks. YAML uses indentation to define structure — mixing tabs and spaces (or using tabs at all) causes parsing errors, invisible formatting bugs, and inconsistent behavior across editors. The fix is simple: always use spaces.

The Error

---
- name: Example playbook
  hosts: all
  tasks:
    - name: Task with tab character
      ansible.builtin.debug:
→       msg: "This line has a tab"  # ← Tab character before msg
$ ansible-lint playbook.yml
no-tabs: Most files should not contain tabs.
playbook.yml:6 Task/Handler: Task with tab character

Why Tabs Break YAML

YAML specification explicitly forbids tab characters for indentation:

"YAML does not allow the use of tabs for indentation." — YAML Spec §4.2

IssueWhat Happens
Tab in indentationYAML parser error: found character '\t' that cannot start any token
Tab in string valueMay pass YAML parsing but ansible-lint flags it
Mixed tabs/spacesLooks aligned in one editor, broken in another
Tab in Jinja2 expressionTemplate rendering errors

The Fix

Replace all tabs with spaces (2 spaces is the Ansible/YAML convention):

---
- name: Example playbook
  hosts: all
  tasks:
    - name: Clean task with spaces
      ansible.builtin.debug:
        msg: "No tabs, just spaces"

Find and Replace Tabs

Using Command Line

# Find files with tabs
grep -rPl '\t' *.yml roles/ playbooks/

# Show tab locations with line numbers
grep -rPn '\t' playbook.yml

# Replace tabs with 2 spaces (in-place)
sed -i 's/\t/  /g' playbook.yml

# Replace across all YAML files
find . -name '*.yml' -exec sed -i 's/\t/  /g' {} +

Using Python

# expand_tabs.py — convert tabs to spaces
import glob
for f in glob.glob('**/*.yml', recursive=True):
    with open(f) as fh:
        content = fh.read()
    if '\t' in content:
        with open(f, 'w') as fh:
            fh.write(content.expandtabs(2))
        print(f'Fixed: {f}')

Configure Your Editor

VS Code

// .vscode/settings.json
{
  "editor.insertSpaces": true,
  "editor.tabSize": 2,
  "editor.detectIndentation": false,
  "[yaml]": {
    "editor.insertSpaces": true,
    "editor.tabSize": 2,
    "editor.renderWhitespace": "all"
  }
}

Vim

" ~/.vimrc
set expandtab       " Convert tabs to spaces
set tabstop=2       " Tab width = 2
set shiftwidth=2    " Indent width = 2
set softtabstop=2   " Backspace through spaces
autocmd FileType yaml setlocal expandtab tabstop=2 shiftwidth=2

.editorconfig (Any Editor)

# .editorconfig
root = true

[*.yml]
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.yaml]
indent_style = space
indent_size = 2

Prevention: Pre-commit Hook

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: check-yaml
      - id: no-commit-to-branch
      - id: mixed-line-ending

  - repo: https://github.com/ansible/ansible-lint
    rev: v6.22.1
    hooks:
      - id: ansible-lint
pip install pre-commit
pre-commit install

Exception: lineinfile Module

Rule 203 does not flag tabs inside ansible.builtin.lineinfile content, because you may legitimately need to insert tab-separated content:

# This is fine — tab is part of the intended file content
- name: Add hosts entry
  ansible.builtin.lineinfile:
    path: /etc/hosts
    line: "192.168.1.10\tweb01.example.com"  # Tab is intentional

yamllint Configuration

Pair ansible-lint with yamllint for comprehensive YAML validation:

# .yamllint
---
extends: default
rules:
  indentation:
    spaces: 2
    indent-sequences: true
  line-length:
    max: 160
  truthy:
    allowed-values: ['true', 'false', 'yes', 'no']

Conclusion

Use spaces, never tabs, in YAML files. Configure your editor to insert spaces with tabstop=2, add .editorconfig to your repo for consistency, and set up pre-commit hooks with ansible-lint to catch tabs before they reach your repository. Use sed -i 's/\t/ /g' or Python's expandtabs(2) to fix existing files in bulk.