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
| Issue | What Happens |
|---|---|
| Tab in indentation | YAML parser error: found character '\t' that cannot start any token |
| Tab in string value | May pass YAML parsing but ansible-lint flags it |
| Mixed tabs/spaces | Looks aligned in one editor, broken in another |
| Tab in Jinja2 expression | Template 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']
Related Articles
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.