diff --git a/docs/man/borg-completion.1 b/docs/man/borg-completion.1
new file mode 100644
index 000000000..0b2f0783d
--- /dev/null
+++ b/docs/man/borg-completion.1
@@ -0,0 +1,76 @@
+.\" Man page generated from reStructuredText.
+.
+.
+.nr rst2man-indent-level 0
+.
+.de1 rstReportMargin
+\\$1 \\n[an-margin]
+level \\n[rst2man-indent-level]
+level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
+-
+\\n[rst2man-indent0]
+\\n[rst2man-indent1]
+\\n[rst2man-indent2]
+..
+.de1 INDENT
+.\" .rstReportMargin pre:
+. RS \\$1
+. nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin]
+. nr rst2man-indent-level +1
+.\" .rstReportMargin post:
+..
+.de UNINDENT
+. RE
+.\" indent \\n[an-margin]
+.\" old: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.nr rst2man-indent-level -1
+.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
+.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
+..
+.TH "BORG-COMPLETION" "1" "2025-11-17" "" "borg backup tool"
+.SH NAME
+borg-completion \- Output shell completion script for the given shell.
+.SH SYNOPSIS
+.sp
+borg [common options] completion [options] SHELL
+.SH DESCRIPTION
+.sp
+This command prints a shell completion script for the given shell.
+.sp
+Please note that for some dynamic completions (like archive IDs), the shell
+completion script will call borg to query the repository. This will work best
+if that call can be made without prompting for user input, so you may want to
+set BORG_REPO and BORG_PASSPHRASE environment variables.
+.SH OPTIONS
+.sp
+See \fIborg\-common(1)\fP for common options of Borg commands.
+.SS arguments
+.INDENT 0.0
+.TP
+.B SHELL
+shell to generate completion for (one of: %(choices)s)
+.UNINDENT
+.SH EXAMPLES
+.sp
+To activate completion in your current shell session, evaluate the output
+of this command. To enable it persistently, add the corresponding line to
+your shell\(aqs startup file.
+.INDENT 0.0
+.INDENT 3.5
+.sp
+.EX
+# Bash (in ~/.bashrc)
+eval \(dq$(borg completion bash)\(dq
+
+# Zsh (in ~/.zshrc)
+eval \(dq$(borg completion zsh)\(dq
+.EE
+.UNINDENT
+.UNINDENT
+.SH SEE ALSO
+.sp
+\fIborg\-common(1)\fP
+.SH AUTHOR
+The Borg Collective
+.\" Generated by docutils manpage writer.
+.
diff --git a/docs/usage.rst b/docs/usage.rst
index a07b3165b..ce9a82ea4 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -68,5 +68,6 @@ Usage
usage/benchmark
usage/help
+ usage/completion
usage/debug
usage/notes
diff --git a/docs/usage/completion.rst b/docs/usage/completion.rst
new file mode 100644
index 000000000..04d554c8f
--- /dev/null
+++ b/docs/usage/completion.rst
@@ -0,0 +1,17 @@
+.. include:: completion.rst.inc
+
+Examples
+~~~~~~~~
+
+To activate completion in your current shell session, evaluate the output
+of this command. To enable it persistently, add the corresponding line to
+your shell's startup file.
+
+::
+
+ # Bash (in ~/.bashrc)
+ eval "$(borg completion bash)"
+
+ # Zsh (in ~/.zshrc)
+ eval "$(borg completion zsh)"
+
diff --git a/docs/usage/completion.rst.inc b/docs/usage/completion.rst.inc
new file mode 100644
index 000000000..9ce0720d3
--- /dev/null
+++ b/docs/usage/completion.rst.inc
@@ -0,0 +1,50 @@
+.. IMPORTANT: this file is auto-generated from borg's built-in help, do not edit!
+
+.. _borg_completion:
+
+borg completion
+---------------
+.. code-block:: none
+
+ borg [common options] completion [options] SHELL
+
+.. only:: html
+
+ .. class:: borg-options-table
+
+ +-------------------------------------------------------+-----------+--------------------------------------------------------+
+ | **positional arguments** |
+ +-------------------------------------------------------+-----------+--------------------------------------------------------+
+ | | ``SHELL`` | shell to generate completion for (one of: %(choices)s) |
+ +-------------------------------------------------------+-----------+--------------------------------------------------------+
+ | .. class:: borg-common-opt-ref |
+ | |
+ | :ref:`common_options` |
+ +-------------------------------------------------------+-----------+--------------------------------------------------------+
+
+ .. raw:: html
+
+
+
+.. only:: latex
+
+ SHELL
+ shell to generate completion for (one of: %(choices)s)
+
+
+ :ref:`common_options`
+ |
+
+Description
+~~~~~~~~~~~
+
+This command prints a shell completion script for the given shell.
+
+Please note that for some dynamic completions (like archive IDs), the shell
+completion script will call borg to query the repository. This will work best
+if that call can be made without prompting for user input, so you may want to
+set BORG_REPO and BORG_PASSPHRASE environment variables.
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index b8ec2784f..2a585cc7a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -37,6 +37,7 @@ dependencies = [
"platformdirs >=3.0.0, <5.0.0; sys_platform == 'darwin'", # for macOS: breaking changes in 3.0.0.
"platformdirs >=2.6.0, <5.0.0; sys_platform != 'darwin'", # for others: 2.6+ works consistently.
"argon2-cffi",
+ "shtab>=1.7.0",
]
[project.optional-dependencies]
diff --git a/src/borg/archiver/__init__.py b/src/borg/archiver/__init__.py
index ce0b1e530..999ba8e1c 100644
--- a/src/borg/archiver/__init__.py
+++ b/src/borg/archiver/__init__.py
@@ -79,6 +79,7 @@ from .analyze_cmd import AnalyzeMixIn
from .benchmark_cmd import BenchmarkMixIn
from .check_cmd import CheckMixIn
from .compact_cmd import CompactMixIn
+from .completion_cmd import CompletionMixIn
from .create_cmd import CreateMixIn
from .debug_cmd import DebugMixIn
from .delete_cmd import DeleteMixIn
@@ -112,6 +113,7 @@ class Archiver(
BenchmarkMixIn,
CheckMixIn,
CompactMixIn,
+ CompletionMixIn,
CreateMixIn,
DebugMixIn,
DeleteMixIn,
@@ -354,6 +356,7 @@ class Archiver(
self.build_parser_benchmarks(subparsers, common_parser, mid_common_parser)
self.build_parser_check(subparsers, common_parser, mid_common_parser)
self.build_parser_compact(subparsers, common_parser, mid_common_parser)
+ self.build_parser_completion(subparsers, common_parser, mid_common_parser)
self.build_parser_create(subparsers, common_parser, mid_common_parser)
self.build_parser_debug(subparsers, common_parser, mid_common_parser)
self.build_parser_delete(subparsers, common_parser, mid_common_parser)
diff --git a/src/borg/archiver/completion_cmd.py b/src/borg/archiver/completion_cmd.py
new file mode 100644
index 000000000..949c79105
--- /dev/null
+++ b/src/borg/archiver/completion_cmd.py
@@ -0,0 +1,198 @@
+import argparse
+
+import shtab
+
+from ._common import process_epilog
+from ..constants import * # NOQA
+from ..helpers import archivename_validator # used to detect ARCHIVE args for dynamic completion
+
+# Dynamic completion for archive IDs (aid:...)
+#
+# This integrates with shtab by:
+# - tagging argparse actions that accept an ARCHIVE (identified by type == archivename_validator)
+# with a .complete mapping pointing to our helper function.
+# - using shtab.complete's 'preamble' parameter to inject the helper into the
+# generated completion script for supported shells.
+#
+# Notes / constraints (per plan):
+# - Calls `borg repo-list --format ...` and filters results by the typed aid: hex prefix.
+# - Non-interactive only. We rely on Borg to fail fast without prompting in non-interactive contexts.
+# If it cannot, we simply return no suggestions.
+
+# Name of the helper function inserted into the generated completion script(s)
+AID_BASH_FN_NAME = "_borg_complete_aid"
+AID_ZSH_FN_NAME = "_borg_complete_aid"
+
+# Global bash preamble that is prepended to the generated completion script.
+# It aggregates only what we need:
+# - wordbreak fixes for ':' and '=' so tokens like 'aid:' and '--repo=/path' stay intact
+# - a minimal dynamic completion helper for aid: archive IDs
+BASH_PREAMBLE = r"""
+# keep ':' and '=' intact so tokens like 'aid:' and '--repo=/path' stay whole
+if [[ ${COMP_WORDBREAKS-} == *:* ]]; then COMP_WORDBREAKS=${COMP_WORDBREAKS//:}; fi
+if [[ ${COMP_WORDBREAKS-} == *=* ]]; then COMP_WORDBREAKS=${COMP_WORDBREAKS//=}; fi
+
+# Complete aid: archive IDs by querying "borg repo-list --short"
+# Note: we only suggest the first 8 hex digits (short ID) for completion.
+_borg_complete_aid() {
+ local cur="${COMP_WORDS[COMP_CWORD]}"
+ [[ "$cur" == aid:* ]] || return 0
+
+ local prefix="${cur#aid:}"
+ [[ -n "$prefix" && ! "$prefix" =~ ^[0-9a-fA-F]*$ ]] && return 0
+
+ # derive repo context from words: --repo=V, --repo V, -r=V, -rV, or -r V
+ local repo_arg=()
+ local i w
+ for (( i=0; i<${#COMP_WORDS[@]}; i++ )); do
+ w="${COMP_WORDS[i]}"
+ if [[ "$w" == --repo=* ]]; then repo_arg=( --repo "${w#--repo=}" ); break
+ elif [[ "$w" == -r=* ]]; then repo_arg=( -r "${w#-r=}" ); break
+ elif [[ "$w" == -r* && "$w" != "-r" ]]; then repo_arg=( -r "${w#-r}" ); break
+ elif [[ "$w" == "--repo" || "$w" == "-r" ]]; then
+ if (( i+1 < ${#COMP_WORDS[@]} )); then repo_arg=( "$w" "${COMP_WORDS[i+1]}" ); fi
+ break
+ fi
+ done
+
+ # ask borg for raw IDs; avoid prompts and suppress stderr
+ local out
+ if [[ -n "${repo_arg[*]}" ]]; then
+ out=$( borg repo-list "${repo_arg[@]}" --short 2>/dev/null /dev/null archive IDs.
+#
+# Notes:
+# - We use zsh's $words/$CURRENT arrays to inspect the command line.
+# - Candidates are returned via `compadd`.
+# - We try to detect repo context from --repo=V, --repo V, -r=V, -rV, -r V.
+ZSH_PREAMBLE = r"""
+# Complete aid: archive IDs by querying "borg repo-list --short"
+# Note: we only suggest the first 8 hex digits (short ID) for completion.
+_borg_complete_aid() {
+ local cur
+ cur="${words[$CURRENT]}"
+ [[ "$cur" == aid:* ]] || return 0
+
+ local prefix="${cur#aid:}"
+ # allow only hex digits as prefix; empty prefix also allowed (list all)
+ [[ -n "$prefix" && ! "$prefix" == [0-9a-fA-F]# ]] && return 0
+
+ # derive repo context from words: --repo=V, --repo V, -r=V, -rV, or -r V
+ local -a repo_arg=()
+ local i w
+ for i in {1..$#words}; do
+ w="$words[$i]"
+ if [[ "$w" == --repo=* ]]; then repo_arg=( --repo "${w#--repo=}" ); break
+ elif [[ "$w" == -r=* ]]; then repo_arg=( -r "${w#-r=}" ); break
+ elif [[ "$w" == -r* && "$w" != "-r" ]]; then repo_arg=( -r "${w#-r}" ); break
+ elif [[ "$w" == "--repo" || "$w" == "-r" ]]; then
+ if (( i+1 <= $#words )); then repo_arg=( "$w" "${words[$((i+1))]}" ); fi
+ break
+ fi
+ done
+
+ # ask borg for raw IDs; avoid prompts and suppress stderr
+ local out
+ if (( ${#repo_arg[@]} > 0 )); then
+ out=$( borg repo-list "${repo_arg[@]}" --short 2>/dev/null /dev/null