ansible.posix.synchronize task failed with error 'Connection' object has no attribute '_new_stdin'

I have open sourced some Ansible roles I use for my personal projects. Everything was working well until one day running my playbook would always fail on the ansible.posix.synchronize task.

TASK [websites : push example.com contents] **************************************************************************************************************************
[ERROR]: Task failed: 'Connection' object has no attribute '_new_stdin'
Origin: /Users/aikchar/ansible-roles/websites/tasks/rhel.yml:18:3

16   become: yes
17
18 - name: "push {{ domain_name }} contents"
     ^ column 3

fatal: [example.com]: FAILED! => {"changed": false, "msg": "Task failed: 'Connection' object has no attribute '_new_stdin'"}

The Fix

The better fix is to remove older collections. Read on to learn why this worked for me.

$ mv /Users/aikchar/.ansible/collections/ansible_collections /Users/aikchar/.ansible/collections/old_ansible_collections

With that fix applied,

TASK [websites : push example.com contents] ***************************************************************************************************************
changed: [example.com]

Alternative Fix

Set custom value of collections_path in ansible.cfg (/Users/aikchar/infra-as-code/ansible.cfg),

[defaults]
collections_path=/usr/local/Cellar/ansible/13.1.0/libexec/lib/python3.14/site-packages
...

Notice I am using Homebrew to install Ansible. Your path may be different if you are using a different Python installation, e.g. using virtualenvs.

The disadvantage of this fix is that everytime I upgrade Ansible, I have to modify this file.

Another thing to keep in mind is that the value should not be put into quotes, whether single or double. It causes a very weird issue,

# Without quotes (as it should be)
$ ansible-config dump | grep COLLECTIONS_PATHS
COLLECTIONS_PATHS(/Users/aikchar/infra-as-code/ansible.cfg) = ['/usr/local/Cellar/ansible/13.1.0/libexec/lib/python3.14/site-packages']
# With quotes (DO NOT DO)
$ ansible-config dump | grep COLLECTIONS_PATHS
COLLECTIONS_PATHS(/Users/aikchar/infra-as-code/ansible.cfg) = ["/Users/aikchar/infra-as-code/'/usr/local/Cellar/ansible/13.1.0/libexec/lib/python3.14/site-packages'"]

The Reason

The default value of collections_path is {{ ANSIBLE_HOME ~ "/collections:/usr/share/ansible/collections" }}. I had not set ANSIBLE_HOME environment variable, in which case it's set to the user's home directory (/Users/aikchar/.ansible), as seen below,

$ ansible-config dump | grep COLLECTIONS_PATHS
COLLECTIONS_PATHS(default) = ['/Users/aikchar/.ansible/collections', '/usr/share/ansible/collections']

At some point in 2022 I had installed Ansible collection for synchronize in this default path. I don't remember when or how or why; it was just there.

$ ls -lA /Users/aikchar/.ansible/collections/ansible_collections/ansible/posix/plugins/action
total 48
-rw-r--r--  1 aikchar  staff      0 Feb 10  2022 __init__.py
-rw-r--r--  1 aikchar  staff   2658 Feb 10  2022 patch.py
-rw-r--r--  1 aikchar  staff  20202 Feb 10  2022 synchronize.py

The culprit was right in front of me, the version was from 2021.

$ grep -B5 -A5 new_stdin /Users/aikchar/.ansible/collections/ansible_collections/ansible/posix/plugins/action/synchronize.py
        # Delegate to localhost as the source of the rsync unless we've been
        # told (via delegate_to) that a different host is the source of the
        # rsync
        if not use_delegate and remote_transport:
            # Create a connection to localhost to run rsync on
            new_stdin = self._connection._new_stdin

            # Unlike port, there can be only one shell
            localhost_shell = None
            for host in C.LOCALHOST:
                localhost_vars = task_vars['hostvars'].get(host, {})
--
                    break
            else:
                localhost_executable = C.DEFAULT_EXECUTABLE
            self._play_context.executable = localhost_executable

            new_connection = connection_loader.get('local', self._play_context, new_stdin)
            self._connection = new_connection
            # Override _remote_is_local as an instance attribute specifically for the synchronize use case
            # ensuring we set local tmpdir correctly
            self._connection._remote_is_local = True
            self._override_module_replaced_vars(task_vars)

Since there was a matching collection in the default path, it was being used instead of the ones packaged in the Ansible installation,

$ ls -lA /usr/local/Cellar/ansible/13.1.0/libexec/lib/python3.14/site-packages/ansible_collections/ansible/posix/plugins/action
total 56
-rw-r--r--  1 aikchar  admin      0 Dec  9 09:58 __init__.py
-rw-r--r--  1 aikchar  admin   2658 Dec  9 09:58 patch.py
-rw-r--r--  1 aikchar  admin  20928 Dec  9 09:58 synchronize.py

In 2023, new_stdin was deprecated by Ansible. Following that, synchronize removed new_stdin in 2024. So even though the newer versions of Ansible were packaging the version of synchronize that didn't have new_stdin, I was still running into the error.

Workaround

Until I had figured out how to fix the issue, I still needed to be able to push contents of the website to the server. A manual workaround I used was to run rsync like so,

rsync -rv --delete --no-perms -e 'ssh -F /Users/aikchar/infra-as-code/ssh_config' --rsync-path 'sudo /usr/bin/rsync' /Users/aikchar/website-example.com/output/ aikchar@example.com:/srv/www/example.com/