diff -pruN 4.4.0-1/.pre-commit-config.yaml 4.5.0-1/.pre-commit-config.yaml
--- 4.4.0-1/.pre-commit-config.yaml	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/.pre-commit-config.yaml	2025-11-22 21:02:16.000000000 +0000
@@ -24,7 +24,7 @@ repos:
     hooks:
     -   id: add-trailing-comma
 -   repo: https://github.com/asottile/pyupgrade
-    rev: v3.21.0
+    rev: v3.21.1
     hooks:
     -   id: pyupgrade
         args: [--py310-plus]
diff -pruN 4.4.0-1/CHANGELOG.md 4.5.0-1/CHANGELOG.md
--- 4.4.0-1/CHANGELOG.md	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/CHANGELOG.md	2025-11-22 21:02:16.000000000 +0000
@@ -1,3 +1,10 @@
+4.5.0 - 2025-11-22
+==================
+
+### Features
+- Add `pre-commit hazmat`.
+    - #3585 PR by @asottile.
+
 4.4.0 - 2025-11-08
 ==================
 
diff -pruN 4.4.0-1/debian/changelog 4.5.0-1/debian/changelog
--- 4.4.0-1/debian/changelog	2025-11-12 06:01:31.000000000 +0000
+++ 4.5.0-1/debian/changelog	2025-11-24 18:00:25.000000000 +0000
@@ -1,3 +1,9 @@
+pre-commit (4.5.0-1) sid; urgency=medium
+
+  * Merging upstream version 4.5.0.
+
+ -- Daniel Baumann <daniel@debian.org>  Mon, 24 Nov 2025 19:00:25 +0100
+
 pre-commit (4.4.0-1) sid; urgency=medium
 
   * Removing rules-requires-root, not needed anymore.
diff -pruN 4.4.0-1/pre_commit/clientlib.py 4.5.0-1/pre_commit/clientlib.py
--- 4.4.0-1/pre_commit/clientlib.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/pre_commit/clientlib.py	2025-11-22 21:02:16.000000000 +0000
@@ -270,10 +270,19 @@ class InvalidManifestError(FatalError):
     pass
 
 
+def _load_manifest_forward_compat(contents: str) -> object:
+    obj = yaml_load(contents)
+    if isinstance(obj, dict):
+        check_min_version('5')
+        raise AssertionError('unreachable')
+    else:
+        return obj
+
+
 load_manifest = functools.partial(
     cfgv.load_from_filename,
     schema=MANIFEST_SCHEMA,
-    load_strategy=yaml_load,
+    load_strategy=_load_manifest_forward_compat,
     exc_tp=InvalidManifestError,
 )
 
diff -pruN 4.4.0-1/pre_commit/commands/gc.py 4.5.0-1/pre_commit/commands/gc.py
--- 4.4.0-1/pre_commit/commands/gc.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/pre_commit/commands/gc.py	2025-11-22 21:02:16.000000000 +0000
@@ -12,6 +12,7 @@ from pre_commit.clientlib import load_ma
 from pre_commit.clientlib import LOCAL
 from pre_commit.clientlib import META
 from pre_commit.store import Store
+from pre_commit.util import rmtree
 
 
 def _mark_used_repos(
@@ -26,7 +27,8 @@ def _mark_used_repos(
         for hook in repo['hooks']:
             deps = hook.get('additional_dependencies')
             unused_repos.discard((
-                store.db_repo_name(repo['repo'], deps), C.LOCAL_REPO_VERSION,
+                store.db_repo_name(repo['repo'], deps),
+                C.LOCAL_REPO_VERSION,
             ))
     else:
         key = (repo['repo'], repo['rev'])
@@ -56,34 +58,41 @@ def _mark_used_repos(
             ))
 
 
-def _gc_repos(store: Store) -> int:
-    configs = store.select_all_configs()
-    repos = store.select_all_repos()
-
-    # delete config paths which do not exist
-    dead_configs = [p for p in configs if not os.path.exists(p)]
-    live_configs = [p for p in configs if os.path.exists(p)]
-
-    all_repos = {(repo, ref): path for repo, ref, path in repos}
-    unused_repos = set(all_repos)
-    for config_path in live_configs:
-        try:
-            config = load_config(config_path)
-        except InvalidConfigError:
-            dead_configs.append(config_path)
-            continue
-        else:
-            for repo in config['repos']:
-                _mark_used_repos(store, all_repos, unused_repos, repo)
+def _gc(store: Store) -> int:
+    with store.exclusive_lock(), store.connect() as db:
+        store._create_configs_table(db)
+
+        repos = db.execute('SELECT repo, ref, path FROM repos').fetchall()
+        all_repos = {(repo, ref): path for repo, ref, path in repos}
+        unused_repos = set(all_repos)
+
+        configs_rows = db.execute('SELECT path FROM configs').fetchall()
+        configs = [path for path, in configs_rows]
+
+        dead_configs = []
+        for config_path in configs:
+            try:
+                config = load_config(config_path)
+            except InvalidConfigError:
+                dead_configs.append(config_path)
+                continue
+            else:
+                for repo in config['repos']:
+                    _mark_used_repos(store, all_repos, unused_repos, repo)
+
+        paths = [(path,) for path in dead_configs]
+        db.executemany('DELETE FROM configs WHERE path = ?', paths)
+
+        db.executemany(
+            'DELETE FROM repos WHERE repo = ? and ref = ?',
+            sorted(unused_repos),
+        )
+        for k in unused_repos:
+            rmtree(all_repos[k])
 
-    store.delete_configs(dead_configs)
-    for db_repo_name, ref in unused_repos:
-        store.delete_repo(db_repo_name, ref, all_repos[(db_repo_name, ref)])
-    return len(unused_repos)
+        return len(unused_repos)
 
 
 def gc(store: Store) -> int:
-    with store.exclusive_lock():
-        repos_removed = _gc_repos(store)
-    output.write_line(f'{repos_removed} repo(s) removed.')
+    output.write_line(f'{_gc(store)} repo(s) removed.')
     return 0
diff -pruN 4.4.0-1/pre_commit/commands/hazmat.py 4.5.0-1/pre_commit/commands/hazmat.py
--- 4.4.0-1/pre_commit/commands/hazmat.py	1970-01-01 00:00:00.000000000 +0000
+++ 4.5.0-1/pre_commit/commands/hazmat.py	2025-11-22 21:02:16.000000000 +0000
@@ -0,0 +1,95 @@
+from __future__ import annotations
+
+import argparse
+import subprocess
+from collections.abc import Sequence
+
+from pre_commit.parse_shebang import normalize_cmd
+
+
+def add_parsers(parser: argparse.ArgumentParser) -> None:
+    subparsers = parser.add_subparsers(dest='tool')
+
+    cd_parser = subparsers.add_parser(
+        'cd', help='cd to a subdir and run the command',
+    )
+    cd_parser.add_argument('subdir')
+    cd_parser.add_argument('cmd', nargs=argparse.REMAINDER)
+
+    ignore_exit_code_parser = subparsers.add_parser(
+        'ignore-exit-code', help='run the command but ignore the exit code',
+    )
+    ignore_exit_code_parser.add_argument('cmd', nargs=argparse.REMAINDER)
+
+    n1_parser = subparsers.add_parser(
+        'n1', help='run the command once per filename',
+    )
+    n1_parser.add_argument('cmd', nargs=argparse.REMAINDER)
+
+
+def _cmd_filenames(cmd: tuple[str, ...]) -> tuple[
+    tuple[str, ...],
+    tuple[str, ...],
+]:
+    for idx, val in enumerate(reversed(cmd)):
+        if val == '--':
+            split = len(cmd) - idx
+            break
+    else:
+        raise SystemExit('hazmat entry must end with `--`')
+
+    return cmd[:split - 1], cmd[split:]
+
+
+def cd(subdir: str, cmd: tuple[str, ...]) -> int:
+    cmd, filenames = _cmd_filenames(cmd)
+
+    prefix = f'{subdir}/'
+    new_filenames = []
+    for filename in filenames:
+        if not filename.startswith(prefix):
+            raise SystemExit(f'unexpected file without {prefix=}: {filename}')
+        else:
+            new_filenames.append(filename.removeprefix(prefix))
+
+    cmd = normalize_cmd(cmd)
+    return subprocess.call((*cmd, *new_filenames), cwd=subdir)
+
+
+def ignore_exit_code(cmd: tuple[str, ...]) -> int:
+    cmd = normalize_cmd(cmd)
+    subprocess.call(cmd)
+    return 0
+
+
+def n1(cmd: tuple[str, ...]) -> int:
+    cmd, filenames = _cmd_filenames(cmd)
+    cmd = normalize_cmd(cmd)
+    ret = 0
+    for filename in filenames:
+        ret |= subprocess.call((*cmd, filename))
+    return ret
+
+
+def impl(args: argparse.Namespace) -> int:
+    args.cmd = tuple(args.cmd)
+    if args.tool == 'cd':
+        return cd(args.subdir, args.cmd)
+    elif args.tool == 'ignore-exit-code':
+        return ignore_exit_code(args.cmd)
+    elif args.tool == 'n1':
+        return n1(args.cmd)
+    else:
+        raise NotImplementedError(f'unexpected tool: {args.tool}')
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+    parser = argparse.ArgumentParser()
+    add_parsers(parser)
+    args = parser.parse_args(argv)
+
+    return impl(args)
+
+
+if __name__ == '__main__':
+    raise SystemExit(main())
diff -pruN 4.4.0-1/pre_commit/lang_base.py 4.5.0-1/pre_commit/lang_base.py
--- 4.4.0-1/pre_commit/lang_base.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/pre_commit/lang_base.py	2025-11-22 21:02:16.000000000 +0000
@@ -5,6 +5,7 @@ import os
 import random
 import re
 import shlex
+import sys
 from collections.abc import Generator
 from collections.abc import Sequence
 from typing import Any
@@ -171,7 +172,10 @@ def run_xargs(
 
 
 def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]:
-    return (*shlex.split(entry), *args)
+    cmd = shlex.split(entry)
+    if cmd[:2] == ['pre-commit', 'hazmat']:
+        cmd = [sys.executable, '-m', 'pre_commit.commands.hazmat', *cmd[2:]]
+    return (*cmd, *args)
 
 
 def basic_run_hook(
diff -pruN 4.4.0-1/pre_commit/main.py 4.5.0-1/pre_commit/main.py
--- 4.4.0-1/pre_commit/main.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/pre_commit/main.py	2025-11-22 21:02:16.000000000 +0000
@@ -10,6 +10,7 @@ import pre_commit.constants as C
 from pre_commit import clientlib
 from pre_commit import git
 from pre_commit.color import add_color_option
+from pre_commit.commands import hazmat
 from pre_commit.commands.autoupdate import autoupdate
 from pre_commit.commands.clean import clean
 from pre_commit.commands.gc import gc
@@ -41,7 +42,7 @@ os.environ.pop('__PYVENV_LAUNCHER__', No
 os.environ.pop('PYTHONEXECUTABLE', None)
 
 COMMANDS_NO_GIT = {
-    'clean', 'gc', 'init-templatedir', 'sample-config',
+    'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config',
     'validate-config', 'validate-manifest',
 }
 
@@ -245,6 +246,11 @@ def main(argv: Sequence[str] | None = No
 
     _add_cmd('gc', help='Clean unused cached repos.')
 
+    hazmat_parser = _add_cmd(
+        'hazmat', help='Composable tools for rare use in hook `entry`.',
+    )
+    hazmat.add_parsers(hazmat_parser)
+
     init_templatedir_parser = _add_cmd(
         'init-templatedir',
         help=(
@@ -389,6 +395,8 @@ def main(argv: Sequence[str] | None = No
             return clean(store)
         elif args.command == 'gc':
             return gc(store)
+        elif args.command == 'hazmat':
+            return hazmat.impl(args)
         elif args.command == 'hook-impl':
             return hook_impl(
                 store,
diff -pruN 4.4.0-1/pre_commit/store.py 4.5.0-1/pre_commit/store.py
--- 4.4.0-1/pre_commit/store.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/pre_commit/store.py	2025-11-22 21:02:16.000000000 +0000
@@ -17,7 +17,6 @@ from pre_commit.util import CalledProces
 from pre_commit.util import clean_path_on_failure
 from pre_commit.util import cmd_output_b
 from pre_commit.util import resource_text
-from pre_commit.util import rmtree
 
 
 logger = logging.getLogger('pre_commit')
@@ -96,7 +95,7 @@ class Store:
                     '    PRIMARY KEY (repo, ref)'
                     ');',
                 )
-                self._create_config_table(db)
+                self._create_configs_table(db)
 
             # Atomic file move
             os.replace(tmpfile, self.db_path)
@@ -215,7 +214,7 @@ class Store:
             'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo,
         )
 
-    def _create_config_table(self, db: sqlite3.Connection) -> None:
+    def _create_configs_table(self, db: sqlite3.Connection) -> None:
         db.executescript(
             'CREATE TABLE IF NOT EXISTS configs ('
             '   path TEXT NOT NULL,'
@@ -232,28 +231,5 @@ class Store:
             return
         with self.connect() as db:
             # TODO: eventually remove this and only create in _create
-            self._create_config_table(db)
+            self._create_configs_table(db)
             db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))
-
-    def select_all_configs(self) -> list[str]:
-        with self.connect() as db:
-            self._create_config_table(db)
-            rows = db.execute('SELECT path FROM configs').fetchall()
-            return [path for path, in rows]
-
-    def delete_configs(self, configs: list[str]) -> None:
-        with self.connect() as db:
-            rows = [(path,) for path in configs]
-            db.executemany('DELETE FROM configs WHERE path = ?', rows)
-
-    def select_all_repos(self) -> list[tuple[str, str, str]]:
-        with self.connect() as db:
-            return db.execute('SELECT repo, ref, path from repos').fetchall()
-
-    def delete_repo(self, db_repo_name: str, ref: str, path: str) -> None:
-        with self.connect() as db:
-            db.execute(
-                'DELETE FROM repos WHERE repo = ? and ref = ?',
-                (db_repo_name, ref),
-            )
-        rmtree(path)
diff -pruN 4.4.0-1/setup.cfg 4.5.0-1/setup.cfg
--- 4.4.0-1/setup.cfg	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/setup.cfg	2025-11-22 21:02:16.000000000 +0000
@@ -1,6 +1,6 @@
 [metadata]
 name = pre_commit
-version = 4.4.0
+version = 4.5.0
 description = A framework for managing and maintaining multi-language pre-commit hooks.
 long_description = file: README.md
 long_description_content_type = text/markdown
diff -pruN 4.4.0-1/tests/clientlib_test.py 4.5.0-1/tests/clientlib_test.py
--- 4.4.0-1/tests/clientlib_test.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/tests/clientlib_test.py	2025-11-22 21:02:16.000000000 +0000
@@ -12,6 +12,8 @@ from pre_commit.clientlib import CONFIG_
 from pre_commit.clientlib import CONFIG_REPO_DICT
 from pre_commit.clientlib import CONFIG_SCHEMA
 from pre_commit.clientlib import DEFAULT_LANGUAGE_VERSION
+from pre_commit.clientlib import InvalidManifestError
+from pre_commit.clientlib import load_manifest
 from pre_commit.clientlib import MANIFEST_HOOK_DICT
 from pre_commit.clientlib import MANIFEST_SCHEMA
 from pre_commit.clientlib import META_HOOK_DICT
@@ -588,3 +590,18 @@ def test_config_hook_stages_defaulting()
         'id': 'fake-hook',
         'stages': ['commit-msg', 'pre-push', 'pre-commit', 'pre-merge-commit'],
     }
+
+
+def test_manifest_v5_forward_compat(tmp_path):
+    manifest = tmp_path.joinpath('.pre-commit-hooks.yaml')
+    manifest.write_text('hooks: {}')
+
+    with pytest.raises(InvalidManifestError) as excinfo:
+        load_manifest(manifest)
+    assert str(excinfo.value) == (
+        f'\n'
+        f'==> File {manifest}\n'
+        f'=====> \n'
+        f'=====> pre-commit version 5 is required but version {C.VERSION} '
+        f'is installed.  Perhaps run `pip install --upgrade pre-commit`.'
+    )
diff -pruN 4.4.0-1/tests/commands/gc_test.py 4.5.0-1/tests/commands/gc_test.py
--- 4.4.0-1/tests/commands/gc_test.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/tests/commands/gc_test.py	2025-11-22 21:02:16.000000000 +0000
@@ -19,11 +19,13 @@ from testing.util import git_commit
 
 
 def _repo_count(store):
-    return len(store.select_all_repos())
+    with store.connect() as db:
+        return db.execute('SELECT COUNT(1) FROM repos').fetchone()[0]
 
 
 def _config_count(store):
-    return len(store.select_all_configs())
+    with store.connect() as db:
+        return db.execute('SELECT COUNT(1) FROM configs').fetchone()[0]
 
 
 def _remove_config_assert_cleared(store, cap_out):
@@ -153,7 +155,8 @@ def test_invalid_manifest_gcd(tempdir_fa
     install_hooks(C.CONFIG_FILE, store)
 
     # we'll "break" the manifest to simulate an old version clone
-    (_, _, path), = store.select_all_repos()
+    with store.connect() as db:
+        path, = db.execute('SELECT path FROM repos').fetchone()
     os.remove(os.path.join(path, C.MANIFEST_FILE))
 
     assert _config_count(store) == 1
@@ -162,3 +165,11 @@ def test_invalid_manifest_gcd(tempdir_fa
     assert _config_count(store) == 1
     assert _repo_count(store) == 0
     assert cap_out.get().splitlines()[-1] == '1 repo(s) removed.'
+
+
+def test_gc_pre_1_14_roll_forward(store, cap_out):
+    with store.connect() as db:  # simulate pre-1.14.0
+        db.executescript('DROP TABLE configs')
+
+    assert not gc(store)
+    assert cap_out.get() == '0 repo(s) removed.\n'
diff -pruN 4.4.0-1/tests/commands/hazmat_test.py 4.5.0-1/tests/commands/hazmat_test.py
--- 4.4.0-1/tests/commands/hazmat_test.py	1970-01-01 00:00:00.000000000 +0000
+++ 4.5.0-1/tests/commands/hazmat_test.py	2025-11-22 21:02:16.000000000 +0000
@@ -0,0 +1,99 @@
+from __future__ import annotations
+
+import sys
+
+import pytest
+
+from pre_commit.commands.hazmat import _cmd_filenames
+from pre_commit.commands.hazmat import main
+from testing.util import cwd
+
+
+def test_cmd_filenames_no_dash_dash():
+    with pytest.raises(SystemExit) as excinfo:
+        _cmd_filenames(('no', 'dashdash', 'here'))
+    msg, = excinfo.value.args
+    assert msg == 'hazmat entry must end with `--`'
+
+
+def test_cmd_filenames_no_filenames():
+    cmd, filenames = _cmd_filenames(('hello', 'world', '--'))
+    assert cmd == ('hello', 'world')
+    assert filenames == ()
+
+
+def test_cmd_filenames_some_filenames():
+    cmd, filenames = _cmd_filenames(('hello', 'world', '--', 'f1', 'f2'))
+    assert cmd == ('hello', 'world')
+    assert filenames == ('f1', 'f2')
+
+
+def test_cmd_filenames_multiple_dashdash():
+    cmd, filenames = _cmd_filenames(('hello', '--', 'arg', '--', 'f1', 'f2'))
+    assert cmd == ('hello', '--', 'arg')
+    assert filenames == ('f1', 'f2')
+
+
+def test_cd_unexpected_filename():
+    with pytest.raises(SystemExit) as excinfo:
+        main(('cd', 'subdir', 'cmd', '--', 'subdir/1', 'not-subdir/2'))
+    msg, = excinfo.value.args
+    assert msg == "unexpected file without prefix='subdir/': not-subdir/2"
+
+
+def _norm(out):
+    return out.replace('\r\n', '\n')
+
+
+def test_cd(tmp_path, capfd):
+    subdir = tmp_path.joinpath('subdir')
+    subdir.mkdir()
+    subdir.joinpath('a').write_text('a')
+    subdir.joinpath('b').write_text('b')
+
+    with cwd(tmp_path):
+        ret = main((
+            'cd', 'subdir',
+            sys.executable, '-c',
+            'import os; print(os.getcwd());'
+            'import sys; [print(open(f).read()) for f in sys.argv[1:]]',
+            '--',
+            'subdir/a', 'subdir/b',
+        ))
+
+    assert ret == 0
+    out, err = capfd.readouterr()
+    assert _norm(out) == f'{subdir}\na\nb\n'
+    assert err == ''
+
+
+def test_ignore_exit_code(capfd):
+    ret = main((
+        'ignore-exit-code', sys.executable, '-c', 'raise SystemExit("bye")',
+    ))
+    assert ret == 0
+    out, err = capfd.readouterr()
+    assert out == ''
+    assert _norm(err) == 'bye\n'
+
+
+def test_n1(capfd):
+    ret = main((
+        'n1', sys.executable, '-c', 'import sys; print(sys.argv[1:])',
+        '--',
+        'foo', 'bar', 'baz',
+    ))
+    assert ret == 0
+    out, err = capfd.readouterr()
+    assert _norm(out) == "['foo']\n['bar']\n['baz']\n"
+    assert err == ''
+
+
+def test_n1_some_error_code():
+    ret = main((
+        'n1', sys.executable, '-c',
+        'import sys; raise SystemExit(sys.argv[1] == "error")',
+        '--',
+        'ok', 'error', 'ok',
+    ))
+    assert ret == 1
diff -pruN 4.4.0-1/tests/lang_base_test.py 4.5.0-1/tests/lang_base_test.py
--- 4.4.0-1/tests/lang_base_test.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/tests/lang_base_test.py	2025-11-22 21:02:16.000000000 +0000
@@ -164,3 +164,15 @@ def test_basic_run_hook(tmp_path):
     assert ret == 0
     out = out.replace(b'\r\n', b'\n')
     assert out == b'hi hello file file file\n'
+
+
+def test_hook_cmd():
+    assert lang_base.hook_cmd('echo hi', ()) == ('echo', 'hi')
+
+
+def test_hook_cmd_hazmat():
+    ret = lang_base.hook_cmd('pre-commit hazmat cd a echo -- b', ())
+    assert ret == (
+        sys.executable, '-m', 'pre_commit.commands.hazmat',
+        'cd', 'a', 'echo', '--', 'b',
+    )
diff -pruN 4.4.0-1/tests/main_test.py 4.5.0-1/tests/main_test.py
--- 4.4.0-1/tests/main_test.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/tests/main_test.py	2025-11-22 21:02:16.000000000 +0000
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import argparse
+import contextlib
 import os.path
 from unittest import mock
 
@@ -8,6 +9,7 @@ import pytest
 
 import pre_commit.constants as C
 from pre_commit import main
+from pre_commit.commands import hazmat
 from pre_commit.errors import FatalError
 from pre_commit.util import cmd_output
 from testing.auto_namedtuple import auto_namedtuple
@@ -97,11 +99,9 @@ CMDS = tuple(fn.replace('_', '-') for fn
 
 @pytest.fixture
 def mock_commands():
-    mcks = {fn: mock.patch.object(main, fn).start() for fn in FNS}
-    ret = auto_namedtuple(**mcks)
-    yield ret
-    for mck in ret:
-        mck.stop()
+    with contextlib.ExitStack() as ctx:
+        mcks = {f: ctx.enter_context(mock.patch.object(main, f)) for f in FNS}
+        yield auto_namedtuple(**mcks)
 
 
 @pytest.fixture
@@ -158,6 +158,17 @@ def test_all_cmds(command, mock_commands
     assert_only_one_mock_called(mock_commands)
 
 
+def test_hazmat(mock_store_dir):
+    with mock.patch.object(hazmat, 'impl') as mck:
+        main.main(('hazmat', 'cd', 'subdir', '--', 'cmd', '--', 'f1', 'f2'))
+    assert mck.call_count == 1
+    (arg,), dct = mck.call_args
+    assert dct == {}
+    assert arg.tool == 'cd'
+    assert arg.subdir == 'subdir'
+    assert arg.cmd == ['cmd', '--', 'f1', 'f2']
+
+
 def test_try_repo(mock_store_dir):
     with mock.patch.object(main, 'try_repo') as patch:
         main.main(('try-repo', '.'))
diff -pruN 4.4.0-1/tests/repository_test.py 4.5.0-1/tests/repository_test.py
--- 4.4.0-1/tests/repository_test.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/tests/repository_test.py	2025-11-22 21:02:16.000000000 +0000
@@ -506,3 +506,14 @@ def test_args_with_spaces_and_quotes(tmp
 
     expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n"
     assert ret == (0, expected)
+
+
+def test_hazmat(tmp_path):
+    ret = run_language(
+        tmp_path, unsupported,
+        f'pre-commit hazmat ignore-exit-code {shlex.quote(sys.executable)} '
+        f"-c 'import sys; raise SystemExit(sys.argv[1:])'",
+        ('f1', 'f2'),
+    )
+    expected = b"['f1', 'f2']\n"
+    assert ret == (0, expected)
diff -pruN 4.4.0-1/tests/store_test.py 4.5.0-1/tests/store_test.py
--- 4.4.0-1/tests/store_test.py	2025-11-08 21:11:43.000000000 +0000
+++ 4.5.0-1/tests/store_test.py	2025-11-22 21:02:16.000000000 +0000
@@ -22,6 +22,17 @@ from testing.util import git_commit
 from testing.util import xfailif_windows
 
 
+def _select_all_configs(store: Store) -> list[str]:
+    with store.connect() as db:
+        rows = db.execute('SELECT * FROM configs').fetchall()
+        return [path for path, in rows]
+
+
+def _select_all_repos(store: Store) -> list[tuple[str, str, str]]:
+    with store.connect() as db:
+        return db.execute('SELECT repo, ref, path FROM repos').fetchall()
+
+
 def test_our_session_fixture_works():
     """There's a session fixture which makes `Store` invariantly raise to
     prevent writing to the home directory.
@@ -91,7 +102,7 @@ def test_clone(store, tempdir_factory, c
     assert git.head_rev(ret) == rev
 
     # Assert there's an entry in the sqlite db for this
-    assert store.select_all_repos() == [(path, rev, ret)]
+    assert _select_all_repos(store) == [(path, rev, ret)]
 
 
 def test_warning_for_deprecated_stages_on_init(store, tempdir_factory, caplog):
@@ -217,7 +228,7 @@ def test_clone_shallow_failure_fallback_
     assert git.head_rev(ret) == rev
 
     # Assert there's an entry in the sqlite db for this
-    assert store.select_all_repos() == [(path, rev, ret)]
+    assert _select_all_repos(store) == [(path, rev, ret)]
 
 
 def test_clone_tag_not_on_mainline(store, tempdir_factory):
@@ -265,7 +276,7 @@ def test_mark_config_as_used(store, tmpd
     with tmpdir.as_cwd():
         f = tmpdir.join('f').ensure()
         store.mark_config_used('f')
-        assert store.select_all_configs() == [f.strpath]
+        assert _select_all_configs(store) == [f.strpath]
 
 
 def test_mark_config_as_used_idempotent(store, tmpdir):
@@ -275,21 +286,12 @@ def test_mark_config_as_used_idempotent(
 
 def test_mark_config_as_used_does_not_exist(store):
     store.mark_config_used('f')
-    assert store.select_all_configs() == []
-
-
-def _simulate_pre_1_14_0(store):
-    with store.connect() as db:
-        db.executescript('DROP TABLE configs')
-
-
-def test_select_all_configs_roll_forward(store):
-    _simulate_pre_1_14_0(store)
-    assert store.select_all_configs() == []
+    assert _select_all_configs(store) == []
 
 
 def test_mark_config_as_used_roll_forward(store, tmpdir):
-    _simulate_pre_1_14_0(store)
+    with store.connect() as db:  # simulate pre-1.14.0
+        db.executescript('DROP TABLE configs')
     test_mark_config_as_used(store, tmpdir)
 
 
@@ -314,7 +316,7 @@ def test_mark_config_as_used_readonly(tm
     assert store.readonly
     # should be skipped due to readonly
     store.mark_config_used(str(cfg))
-    assert store.select_all_configs() == []
+    assert _select_all_configs(store) == []
 
 
 def test_clone_with_recursive_submodules(store, tmp_path):
