--- a/duplicity/dup_main.py
+++ b/duplicity/dup_main.py
@@ -132,24 +132,36 @@ def get_passphrase(n, action, for_signin
         log.Notice(_("Reuse configured SIGN_PASSPHRASE as PASSPHRASE"))
         return os.environ["SIGN_PASSPHRASE"]
 
-    # Not in the environment, check if encryption passphrase is needed
+    # no passphrase if --no-encryption or --use-agent
+    if not config.encryption or config.use_agent:
+        return ""
+
+    # no passphrase if --passphrase* in --gpg-options
+    if "--passphrase" in config.gpg_options:
+        return ""
+
+    # Check if encryption passphrase is needed
     asymmetric = False
     need_passphrase = False
     profile = config.gpg_profile
     encrypt_keys = profile.recipients + profile.hidden_recipients
     if profile.sign_key:
+        # that one very very
         encrypt_keys.append(profile.sign_key)
     if encrypt_keys:
+        # just encrypting to a public key never needs any passphrase. it's a *public* key, FFS!
         asymmetric = True
-        for key in encrypt_keys:
-            if util.key_needs_passphrase(key):
-                log.Notice(f"Key {key} needs passphrase.")
-                need_passphrase = True
-                break
-        else:
-            log.Notice("No encryption keys need passphrase.")
+        # decryption in check_remote of course needs the private half,
+        # and THAT may need a passphrase...
+        if config.check_remote:
+            for key in encrypt_keys:
+                if util.key_needs_passphrase(config.gpg_binary, key):
+                    log.Notice(f"Key {key} needs passphrase.")
+                    need_passphrase = True
+                    break
+                else:
+                    log.Notice("No encryption keys need passphrase.")
     else:
-        symmetric = True
         need_passphrase = True
         log.Notice("No encryption keys configured.")
 
--- a/duplicity/util.py
+++ b/duplicity/util.py
@@ -27,11 +27,13 @@ import atexit
 import csv
 import errno
 import json
+import locale
 import multiprocessing
 import os
 import socket
 import sys
 import traceback
+from contextlib import contextmanager
 from io import StringIO
 
 import fasteners
@@ -203,28 +205,93 @@ def release_lockfile():
             pass
 
 
-def key_needs_passphrase(key):
+def key_needs_passphrase(gpgbin, key, logfile=None):
     """
-    Check if a key needs a passphrase.
+    Determine whether a GnuPG key requires a passphrase.
+
+    This helper invokes the specified GnuPG frontend in a non‑destructive
+    way to discover if the secret key is protected by a passphrase. It uses
+    `pexpect` to spawn the command and watch for prompts or agent errors,
+    never changing the key material itself.
+
+    How it works
+    - Runs: ``<gpgbin> --pinentry-mode cancel --dry-run --change-passphrase <key>``
+      with a C UTF‑8 locale to ensure predictable output.
+    - Interprets the interaction:
+        - If the process reaches EOF without a passphrase prompt, the key is
+          considered not to need a passphrase.
+        - If a passphrase prompt appears (matches ``passphrase.*:``), the key
+          is considered to need a passphrase.
+        - If ``gpg-agent`` fails to start or ignores an inquiry, we log an
+          error and return ``None`` to signal an indeterminate result.
+
+    Parameters
+    - gpgbin: str
+        The GnuPG command to execute, e.g. ``"gpg"`` or ``"gpgsm"``.
+    - key: str
+        The key identifier understood by the given binary. Examples:
+        - For ``gpg`` (OpenPGP): a key ID or fingerprint, e.g. ``"56538CCF"``.
+        - For ``gpgsm`` (S/MIME): a certificate keyref, e.g.
+          ``"\\&165F2FB4F58D..."``.
+    - logfile: a file-like object or ``None``
+        If provided, raw pexpect I/O is mirrored to this stream for debugging
+        (e.g. ``sys.stdout``). Defaults to ``None``.
+
+    Returns
+    - ``True``  if the key requires a passphrase.
+    - ``False`` if the key does not require a passphrase.
+    - ``None``  if the status cannot be determined due to a runtime error
+      (e.g., agent failed to start or pexpect raised an exception).
+
+    Notes
+    - The check is read‑only: ``--dry-run`` and ``--pinentry-mode cancel`` are
+      used to avoid modifying the key or prompting the user.
+    - Environment variables ``LANG`` and ``LC_ALL`` are forced to ``C.utf8``
+      to make output matching stable across locales.
+    - For end‑to‑end manual verification with the repository’s test keyring,
+      see ``testing/manual/needspass.py``.
     """
+
+    environ = {**os.environ, "LANG": "C.utf8", "LC_ALL": "C.utf8"}
+    cmd = f"{gpgbin} --pinentry-mode cancel --dry-run --change-passphrase {key} "
+
+    log.Debug(f"{cmd=}")
+
     try:
-        child = pexpect.spawn("gpg", f"--pinentry-mode=loopback --dry-run --passwd {key}".split())
-    except Exception:
-        log.FatalError(f"Exception spawning gpg while checking if passphrase needed for key: {key}")
+        child = pexpect.spawn(cmd, encoding="utf-8", env=environ)
+        child.logfile = logfile
+    except pexpect.ExceptionPexpect as e:
+        log.Error(f"An unexpected error occurred: {e}")
+        return None
 
     try:
-        got = child.expect(["passphrase.*:", pexpect.EOF])
-    except Exception:
-        log.FatalError(f"Exception while checking if passphrase needed for key: {key}: {str(child)}")
+        got = child.expect(
+            [
+                pexpect.EOF,
+                "passphrase.*:",
+                "failed to start gpg-agent",
+                "ignoring gpg-agent inquiry",
+            ]
+        )
+    except pexpect.ExceptionPexpect as e:
+        log.Error(f"Exception while checking if passphrase needed for: {key}:\n{e}")
+        return None
+
+    child.close()
+    log.Debug(f"{child.exitstatus=}, {child.signalstatus=}, {got=}, {child.after=}")
 
     if got == 0:
-        log.Debug(f"Key {key} needs passphrase")
-        child.close()
-        return True
-    elif got == 1:
         log.Debug(f"Key {key} does not need passphrase")
         return False
-    return None
+    elif got == 1:
+        log.Debug(f"Key {key} needs passphrase")
+        return True
+    elif got == 2:
+        log.Error(f"gpg-agent failed to start.")
+        return None
+    elif got == 3:
+        log.Error(f"gpg-agent failed inquiry ignored.")
+        return None
 
 
 def copyfileobj(infp, outfp, byte_count=-1):
--- a/testing/functional/test_regression.py
+++ b/testing/functional/test_regression.py
@@ -126,6 +126,7 @@ class RegressionTest(FunctionalTestCase)
             ]
         )
 
+    @unittest.skipIf(os.path.exists("/.dockerenv"), "Won't work on docker")
     def test_issue908(self):
         """
         Test issue 908 - gpg: public key decryption failed: No passphrase given (3.0.6.2)
--- a/testing/functional/test_restart.py
+++ b/testing/functional/test_restart.py
@@ -88,6 +88,7 @@ class RestartTest(FunctionalTestCase):
         self.backup("full", f"{_runtest_dir}/testfiles/largefiles")
         self.verify(f"{_runtest_dir}/testfiles/largefiles")
 
+    @unittest.skipIf(os.path.exists("/.dockerenv"), "Won't work on docker")
     def test_restart_encrypt_without_password(self):
         """
         Test that we can successfully restart a encrypt-key-only backup without
--- /dev/null
+++ b/testing/manual/needspass.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""
+Manual helper to verify whether specific GnuPG keys require a passphrase.
+
+What this script does
+---------------------
+- Changes to the project root and sets `GNUPGHOME` to use the test keyring in
+  `testing/gnupg`.
+- Calls `duplicity.util.key_needs_passphrase(gpgbin, key)` for a small set of
+  known test keys with both `gpg` and `gpgsm` and reports results.
+- Prints PASS/FAIL per key and exits with the number of failures as the status
+  code (0 means all checks passed).
+
+How to run
+----------
+- From the project root:
+    - `python3 testing/manual/needspass.py`
+
+Optional debugging
+------------------
+- To see the interaction with `gpg-agent` via `pexpect`, set `logfile=sys.stdout`
+  where indicated in the code (search for the comment “set logfile=sys.stdout”).
+
+Prerequisites
+-------------
+- `gpg`, `gpgsm`, and a functioning `gpg-agent` available in PATH.
+- No additional arguments are needed; the script is self‑contained for the
+  repository’s test keyring.
+
+Notes
+-----
+- The expected outcomes for the embedded test keys are specified in
+  `test_keys` below; modify or extend this list if you need to try other keys.
+"""
+
+import os
+import sys
+
+os.chdir(os.path.dirname(__file__) + "/../..")
+os.environ["GNUPGHOME"] = "testing/gnupg"
+
+from duplicity import log
+from duplicity import util
+
+
+test_keys = [
+    ("gpgsm", "\\&165F2FB4F58D537404FE223A603878F54CD444E5", True),
+    ("gpgsm", "\\&86E23738BB09B27C6C7E4F76C39DA0194586CF4B", True),
+    ("gpg", "56538CCF", True),
+    ("gpg", "B5FA894F", False),
+    ("gpg", "9B736B2A", True),
+]
+
+log.setup()
+log.setverbosity(log.DEBUG)
+
+passed = failed = errored = 0
+
+for gpgbin, key, needs_passphrase in test_keys:
+    # set logfile=sys.stdout to see pexpect output
+    res = util.key_needs_passphrase(gpgbin, key, logfile=None)
+    if res is None:
+        log.Debug(f"HARD FAIL: gpg-agent failed for {key}")
+        errored += 1
+    elif res == needs_passphrase:
+        log.Debug(f"PASS: {key} needs passphrase={needs_passphrase} OK")
+        passed += 1
+    else:
+        log.Debug(f"FAIL: {key} needs passphrase={needs_passphrase} got {res=}")
+        failed += 1
+
+    log.Debug("\n========================================\n")
+
+log.Debug(f"{passed=}, {failed=}, {errored=}")
+
+sys.exit(failed)
