diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1f32ff12..96709a96 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -54,10 +54,8 @@ jobs:
           channels: conda-forge/label/python_rc,conda-forge
       - name: Install Nox-under-test (uv)
         run:  uv pip install --system .
-      - name: Run tests on ${{ matrix.os }} (tox <4)
-        run: nox --non-interactive --error-on-missing-interpreter --session "tests(python='${{ matrix.python-version }}', tox_version='<4')" -- --full-trace
-      - name: Run tox-to-nox tests on ${{ matrix.os }} (tox latest)
-        run: nox --non-interactive --error-on-missing-interpreter --session "tests(python='${{ matrix.python-version }}', tox_version='latest')" -- tests/test_tox_to_nox.py --full-trace
+      - name: Run tests on ${{ matrix.os }}
+        run: nox --non-interactive --error-on-missing-interpreter --session "tests-${{ matrix.python-version }}" -- --full-trace
       - name: Save coverage report
         uses: actions/upload-artifact@v4
         with:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6479a924..2448c5d0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -42,7 +42,7 @@ To just check for lint errors, run:
 
 To run against a particular Python version:
 
-    nox --session "tests(python='3.12', tox_version='latest')"
+    nox --session tests-3.12
     nox --session conda_tests
     nox --session mamba_tests
     nox --session micromamba_tests
diff --git a/nox/tox_to_nox.jinja2 b/nox/tox_to_nox.jinja2
deleted file mode 100644
index 1e22ca89..00000000
--- a/nox/tox_to_nox.jinja2
+++ /dev/null
@@ -1,36 +0,0 @@
-import nox
-
-{% for envname, envconfig in config.envconfigs.items()|sort: %}
-@nox.session({%- if envconfig.basepython %}python='{{envconfig.basepython}}'{%- endif %})
-def {{fixname(envname)}}(session):
-    {%- if envconfig.description != '' %}
-    """{{envconfig.description}}"""
-    {%- endif %}
-    {%- set envs = dict(envconfig.setenv) -%}
-    {%- do envs.pop('PYTHONHASHSEED', None) -%}
-    {%- do envs.pop('TOX_ENV_DIR', None) -%}
-    {%- do envs.pop('TOX_ENV_NAME', None) -%}
-    {%- for key, value in envs.items()|sort: %}
-    session.env['{{key}}'] = '{{value}}'
-    {%- endfor %}
-
-    {%- if envconfig.deps %}
-    session.install({{wrapjoin(envconfig.deps)}})
-    {%- endif %}
-
-    {%- if not envconfig.skip_install %}
-    {%- if envconfig.usedevelop %}
-    session.install('-e', '.')
-    {%- else %}
-    session.install('.')
-    {%- endif -%}
-    {%- endif %}
-
-    {%- if envconfig.changedir and config.toxinidir.bestrelpath(envconfig.changedir) != '.' %}
-    session.chdir('{{config.toxinidir.bestrelpath(envconfig.changedir)}}')
-    {%- endif %}
-
-    {%- for command in envconfig.commands %}
-    session.run({{wrapjoin(command)}})
-    {%- endfor %}
-{% endfor %}
diff --git a/nox/tox_to_nox.py b/nox/tox_to_nox.py
index 088f6664..48844544 100644
--- a/nox/tox_to_nox.py
+++ b/nox/tox_to_nox.py
@@ -21,13 +21,11 @@
 import re
 import sys
 from configparser import ConfigParser
-from importlib import metadata
 from pathlib import Path
 from subprocess import check_output
 from typing import Any, Iterable
 
 import jinja2
-import tox.config
 
 if sys.version_info < (3, 9):
     from importlib_resources import files
@@ -42,22 +40,12 @@ def __dir__() -> list[str]:
     return __all__
 
 
-TOX_VERSION = metadata.version("tox")
-
-TOX4 = int(TOX_VERSION.split(".")[0]) >= 4
-
 DATA = files("nox")
 
-if TOX4:
-    _TEMPLATE = jinja2.Template(
-        DATA.joinpath("tox4_to_nox.jinja2").read_text(encoding="utf-8"),
-        extensions=["jinja2.ext.do"],
-    )
-else:
-    _TEMPLATE = jinja2.Template(
-        DATA.joinpath("tox_to_nox.jinja2").read_text(encoding="utf-8"),
-        extensions=["jinja2.ext.do"],
-    )
+_TEMPLATE = jinja2.Template(
+    DATA.joinpath("tox4_to_nox.jinja2").read_text(encoding="utf-8"),
+    extensions=["jinja2.ext.do"],
+)
 
 
 def wrapjoin(seq: Iterable[Any]) -> str:
@@ -66,14 +54,11 @@ def wrapjoin(seq: Iterable[Any]) -> str:
 
 
 def fixname(envname: str) -> str:
-    """Replace dashes with underscores and check if the result is a valid identifier."""
-    envname = envname.replace("-", "_").replace("testenv:", "")
-    if not envname.isidentifier():
-        print(
-            f"Environment {envname!r} is not a valid nox session name.\n"
-            "Manually update the session name in noxfile.py before running nox."
-        )
-    return envname
+    """
+    Replace dashes with underscores. Tox 4+ requires valid identifiers for
+    names already.
+    """
+    return envname.replace("-", "_").replace("testenv:", "")
 
 
 def write_output_to_file(output: str, filename: str) -> None:
@@ -88,65 +73,60 @@ def main() -> None:
 
     args = parser.parse_args()
 
-    if TOX4:
-        output = check_output(["tox", "config"], text=True, encoding="utf-8")  # noqa: S607
-        original_config = ConfigParser()
-        original_config.read_string(output)
-        config: dict[str, dict[str, Any]] = {}
-
-        for name, section in original_config.items():
-            if name == "DEFAULT":
-                continue
-
-            config[name] = dict(section)
-            # Convert set_env from string to dict
-            set_env = {}
-            for var in section.get("set_env", "").strip().splitlines():
-                k, v = var.split("=")
-                if k not in {
-                    "PYTHONHASHSEED",
-                    "PIP_DISABLE_PIP_VERSION_CHECK",
-                    "PYTHONIOENCODING",
-                }:
-                    set_env[k] = v
-
-            config[name]["set_env"] = set_env
-
-            config[name]["commands"] = [
-                wrapjoin(c.split()) for c in section["commands"].strip().splitlines()
+    output = check_output(["tox", "config"], text=True, encoding="utf-8")  # noqa: S607
+    original_config = ConfigParser()
+    original_config.read_string(output)
+    config: dict[str, dict[str, Any]] = {}
+
+    for name, section in original_config.items():
+        if name == "DEFAULT":
+            continue
+
+        config[name] = dict(section)
+        # Convert set_env from string to dict
+        set_env = {}
+        for var in section.get("set_env", "").strip().splitlines():
+            k, v = var.split("=")
+            if k not in {
+                "PYTHONHASHSEED",
+                "PIP_DISABLE_PIP_VERSION_CHECK",
+                "PYTHONIOENCODING",
+            }:
+                set_env[k] = v
+
+        config[name]["set_env"] = set_env
+
+        config[name]["commands"] = [
+            wrapjoin(c.split()) for c in section["commands"].strip().splitlines()
+        ]
+
+        config[name]["deps"] = wrapjoin(
+            [
+                part
+                for dep in section["deps"].strip().splitlines()
+                for part in (dep.split() if dep.startswith("-r") else [dep])
             ]
+        )
 
-            config[name]["deps"] = wrapjoin([
-                part for dep
-                in section["deps"].strip().splitlines()
-                for part in
-                (dep.split() if dep.startswith("-r") else [dep])
-            ])
-
-            for option in "skip_install", "use_develop":
-                if section.get(option):
-                    if section[option] == "False":
-                        config[name][option] = False
-                    else:
-                        config[name][option] = True
-
-            if os.path.isabs(section["base_python"]) or re.match(
-                r"py\d+", section["base_python"]
-            ):
-                impl = (
-                    "python" if section["py_impl"] == "cpython" else section["py_impl"]
-                )
-                config[name]["base_python"] = impl + section["py_dot_ver"]
-
-            change_dir = Path(section.get("change_dir"))
-            rel_to_cwd = change_dir.relative_to(Path.cwd())
-            if str(rel_to_cwd) == ".":
-                config[name]["change_dir"] = None
-            else:
-                config[name]["change_dir"] = rel_to_cwd
-
-    else:
-        config = tox.config.parseconfig([])
+        for option in "skip_install", "use_develop":
+            if section.get(option):
+                if section[option] == "False":
+                    config[name][option] = False
+                else:
+                    config[name][option] = True
+
+        if os.path.isabs(section["base_python"]) or re.match(
+            r"py\d+", section["base_python"]
+        ):
+            impl = "python" if section["py_impl"] == "cpython" else section["py_impl"]
+            config[name]["base_python"] = impl + section["py_dot_ver"]
+
+        change_dir = Path(section.get("change_dir"))
+        rel_to_cwd = change_dir.relative_to(Path.cwd())
+        if str(rel_to_cwd) == ".":
+            config[name]["change_dir"] = None
+        else:
+            config[name]["change_dir"] = rel_to_cwd
 
     output = _TEMPLATE.render(config=config, wrapjoin=wrapjoin, fixname=fixname)
 
diff --git a/noxfile.py b/noxfile.py
index 7c23c90f..75319631 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -38,28 +38,16 @@
 ]
 
 
-@nox.session
-@nox.parametrize(
-    "python, tox_version",
-    [
-        (python, tox_version)
-        for python in ALL_PYTHONS
-        for tox_version in ("latest", "<4")
-    ],
-)
-def tests(session: nox.Session, tox_version: str) -> None:
+@nox.session(python=ALL_PYTHONS)
+def tests(session: nox.Session) -> None:
     """Run test suite with pytest."""
 
-    coverage_file = (
-        f".coverage.{sys.platform}.{session.python}.tox{tox_version.lstrip('<')}"
-    )
+    coverage_file = f".coverage.{sys.platform}.{session.python}"
 
     session.create_tmp()  # Fixes permission errors on Windows
     session.install(*PYPROJECT["dependency-groups"]["test"], "uv")
     session.install("-e.[tox_to_nox]")
-    if tox_version != "latest":
-        session.install(f"tox{tox_version}")
-    extra_env = {"PYTHONWARNDEFAULTENCODING": "1"} if tox_version == "latest" else {}
+    extra_env = {"PYTHONWARNDEFAULTENCODING": "1"}
     session.run(
         "pytest",
         "--cov",
diff --git a/pyproject.toml b/pyproject.toml
index f0911e8e..c52a5e06 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,7 +50,7 @@ dependencies = [
 optional-dependencies.tox_to_nox = [
   "importlib-resources; python_version<'3.9'",
   "jinja2",
-  "tox",
+  "tox>=4",
 ]
 optional-dependencies.uv = [
   "uv>=0.1.6",
diff --git a/requirements-conda-test.txt b/requirements-conda-test.txt
index 8dde50ea..8392016f 100644
--- a/requirements-conda-test.txt
+++ b/requirements-conda-test.txt
@@ -2,5 +2,5 @@ argcomplete >=1.9.4,<3.0
 colorlog >=2.6.1,<7.0.0
 jinja2
 pytest
-tox<4.0.0
+tox>=4.0.0
 virtualenv >=20.14.1
diff --git a/tests/test_tox_to_nox.py b/tests/test_tox_to_nox.py
index 24885e3c..17751466 100644
--- a/tests/test_tox_to_nox.py
+++ b/tests/test_tox_to_nox.py
@@ -28,7 +28,6 @@
 
 tox_to_nox = pytest.importorskip("nox.tox_to_nox")
 
-TOX4 = tox_to_nox.TOX4
 PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}"
 PYTHON_VERSION_NODOT = PYTHON_VERSION.replace(".", "")
 
@@ -289,44 +288,6 @@ def test_with_dash(session):
     )
 
 
-@pytest.mark.skipif(TOX4, reason="Not supported in tox 4.")
-def test_non_identifier_in_envname(
-    makeconfig: Callable[[str], str], capfd: pytest.CaptureFixture[str]
-) -> None:
-    result = makeconfig(
-        textwrap.dedent(
-            f"""
-            [tox]
-            envlist = test-with-&
-
-            [testenv:test-with-&]
-            basepython = python{PYTHON_VERSION}
-            """
-        )
-    )
-
-    assert (
-        result
-        == textwrap.dedent(
-            f"""
-        import nox
-
-
-        @nox.session(python='python{PYTHON_VERSION}')
-        def test_with_&(session):
-            session.install('.')
-        """
-        ).lstrip()
-    )
-
-    out, _ = capfd.readouterr()
-
-    assert (
-        out == "Environment 'test_with_&' is not a valid nox session name.\n"
-        "Manually update the session name in noxfile.py before running nox.\n"
-    )
-
-
 def test_descriptions_into_docstrings(makeconfig: Callable[[str], str]) -> None:
     result = makeconfig(
         textwrap.dedent(
@@ -371,17 +332,16 @@ def test_commands_with_requirements(makeconfig: Callable[[str], str]) -> None:
             pytest
             pytest-cov
             aiohttp: -r requirements/aiohttp.txt
-    """
-        )
+    """)
     )
 
     assert (
         result
-        == textwrap.dedent("""
+        == textwrap.dedent(f"""
     import nox
 
 
-    @nox.session(python='python3.13')
+    @nox.session(python='python{PYTHON_VERSION}')
     def aiohttp(session):
         session.install('pytest', 'pytest-cov', '-r', 'requirements/aiohttp.txt')
         session.install('-e', '.')