asottile: Cool, I have some plans to do some larger changes and get to "v1.0.0", I think I'll wait to merge this until I'm ready to do that -- maybe a few days.
diff --git a/.travis.yml b/.travis.yml
index e84d8cc..8f91d70 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -28,4 +28,4 @@ after_success: coveralls
 cache:
     directories:
         - $HOME/.cache/pip
-        - $HOME/.pre-commit
+        - $HOME/.cache/pre-commit
diff --git a/appveyor.yml b/appveyor.yml
index 013e142..ddb9af3 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -23,4 +23,4 @@ test_script: tox
 
 cache:
   - '%LOCALAPPDATA%\pip\cache'
-  - '%USERPROFILE%\.pre-commit'
+  - '%USERPROFILE%\.cache\pre-commit'
diff --git a/pre_commit/commands/clean.py b/pre_commit/commands/clean.py
index 8cea6fc..75d0acc 100644
--- a/pre_commit/commands/clean.py
+++ b/pre_commit/commands/clean.py
@@ -8,7 +8,9 @@ from pre_commit.util import rmtree
 
 
 def clean(runner):
-    if os.path.exists(runner.store.directory):
-        rmtree(runner.store.directory)
-        output.write_line('Cleaned {}.'.format(runner.store.directory))
+    legacy_path = os.path.expanduser('~/.pre-commit')
+    for directory in (runner.store.directory, legacy_path):
+        if os.path.exists(directory):
+            rmtree(directory)
+            output.write_line('Cleaned {}.'.format(directory))
     return 0
diff --git a/pre_commit/error_handler.py b/pre_commit/error_handler.py
index b248f93..76662e9 100644
--- a/pre_commit/error_handler.py
+++ b/pre_commit/error_handler.py
@@ -28,10 +28,11 @@ def _log_and_exit(msg, exc, formatted):
         _to_bytes(exc), b'\n',
     ))
     output.write(error_msg)
-    output.write_line('Check the log at ~/.pre-commit/pre-commit.log')
     store = Store()
     store.require_created()
-    with open(os.path.join(store.directory, 'pre-commit.log'), 'wb') as log:
+    log_path = os.path.join(store.directory, 'pre-commit.log')
+    output.write_line('Check the log at {}'.format(log_path))
+    with open(log_path, 'wb') as log:
         output.write(error_msg, stream=log)
         output.write_line(formatted, stream=log)
     raise SystemExit(1)
diff --git a/pre_commit/store.py b/pre_commit/store.py
index 263b315..3262bda 100644
--- a/pre_commit/store.py
+++ b/pre_commit/store.py
@@ -29,9 +29,9 @@ def _get_default_directory():
     `Store.get_default_directory` can be mocked in tests and
     `_get_default_directory` can be tested.
     """
-    return os.environ.get(
-        'PRE_COMMIT_HOME',
-        os.path.join(os.path.expanduser('~'), '.pre-commit'),
+    return os.environ.get('PRE_COMMIT_HOME') or os.path.join(
+        os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'),
+        'pre-commit',
     )
 
 

pre-commit should meet the XDG Base Directory Specification
XDG Base Directory Specification is quite common now. Just `ls ~/.cache ~/.config ~/.local` to realize it.

I think `~/.pre-commit` should be moved to `$XDG_CACHE_HOME` or `$HOME/.cache`

https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
diff --git a/tests/commands/clean_test.py b/tests/commands/clean_test.py
index bdbdc99..fddd444 100644
--- a/tests/commands/clean_test.py
+++ b/tests/commands/clean_test.py
@@ -2,18 +2,35 @@ from __future__ import unicode_literals
 
 import os.path
 
+import mock
+import pytest
+
 from pre_commit.commands.clean import clean
 from pre_commit.util import rmtree
 
 
-def test_clean(runner_with_mocked_store):
+@pytest.fixture(autouse=True)
+def fake_old_dir(tempdir_factory):
+    fake_old_dir = tempdir_factory.get()
+
+    def _expanduser(path, *args, **kwargs):
+        assert path == '~/.pre-commit'
+        return fake_old_dir
+
+    with mock.patch.object(os.path, 'expanduser', side_effect=_expanduser):
+        yield fake_old_dir
+
+
+def test_clean(runner_with_mocked_store, fake_old_dir):
+    assert os.path.exists(fake_old_dir)
     assert os.path.exists(runner_with_mocked_store.store.directory)
     clean(runner_with_mocked_store)
+    assert not os.path.exists(fake_old_dir)
     assert not os.path.exists(runner_with_mocked_store.store.directory)
 
 
 def test_clean_empty(runner_with_mocked_store):
-    """Make sure clean succeeds when we the directory doesn't exist."""
+    """Make sure clean succeeds when the directory doesn't exist."""
     rmtree(runner_with_mocked_store.store.directory)
     assert not os.path.exists(runner_with_mocked_store.store.directory)
     clean(runner_with_mocked_store)
diff --git a/tests/error_handler_test.py b/tests/error_handler_test.py
index bdc54b6..d6eaf50 100644
--- a/tests/error_handler_test.py
+++ b/tests/error_handler_test.py
@@ -81,12 +81,12 @@ def test_log_and_exit(cap_out, mock_out_store_directory):
         )
 
     printed = cap_out.get()
+    log_file = os.path.join(mock_out_store_directory, 'pre-commit.log')
     assert printed == (
         'msg: FatalError: hai\n'
-        'Check the log at ~/.pre-commit/pre-commit.log\n'
+        'Check the log at {}\n'.format(log_file)
     )
 
-    log_file = os.path.join(mock_out_store_directory, 'pre-commit.log')
     assert os.path.exists(log_file)
     contents = io.open(log_file).read()
     assert contents == (
@@ -102,6 +102,7 @@ def test_error_handler_non_ascii_exception(mock_out_store_directory):
 
 
 def test_error_handler_no_tty(tempdir_factory):
+    pre_commit_home = tempdir_factory.get()
     output = cmd_output_mocked_pre_commit_home(
         sys.executable, '-c',
         'from __future__ import unicode_literals\n'
@@ -110,8 +111,10 @@ def test_error_handler_no_tty(tempdir_factory):
         '    raise ValueError("\\u2603")\n',
         retcode=1,
         tempdir_factory=tempdir_factory,
+        pre_commit_home=pre_commit_home,
     )
+    log_file = os.path.join(pre_commit_home, 'pre-commit.log')
     assert output[1].replace('\r', '') == (
         'An unexpected error has occurred: ValueError: ☃\n'
-        'Check the log at ~/.pre-commit/pre-commit.log\n'
+        'Check the log at {}\n'.format(log_file)
     )
diff --git a/tests/main_test.py b/tests/main_test.py
index 4348b8c..29c9ea2 100644
--- a/tests/main_test.py
+++ b/tests/main_test.py
@@ -2,6 +2,7 @@ from __future__ import absolute_import
 from __future__ import unicode_literals
 
 import argparse
+import os.path
 
 import mock
 import pytest
@@ -121,10 +122,11 @@ def test_expected_fatal_error_no_git_repo(
     with cwd(tempdir_factory.get()):
         with pytest.raises(SystemExit):
             main.main([])
+    log_file = os.path.join(mock_out_store_directory, 'pre-commit.log')
     assert cap_out.get() == (
         'An error has occurred: FatalError: git failed. '
         'Is it installed, and are you in a Git repository directory?\n'
-        'Check the log at ~/.pre-commit/pre-commit.log\n'
+        'Check the log at {}\n'.format(log_file)
     )
 
 
diff --git a/tests/store_test.py b/tests/store_test.py
index 106a464..718f24d 100644
--- a/tests/store_test.py
+++ b/tests/store_test.py
@@ -29,7 +29,15 @@ def test_our_session_fixture_works():
 def test_get_default_directory_defaults_to_home():
     # Not we use the module level one which is not mocked
     ret = _get_default_directory()
-    assert ret == os.path.join(os.path.expanduser('~'), '.pre-commit')
+    assert ret == os.path.join(os.path.expanduser('~/.cache'), 'pre-commit')
+
+
+def test_adheres_to_xdg_specification():
+    with mock.patch.dict(
+        os.environ, {'XDG_CACHE_HOME': '/tmp/fakehome'},
+    ):
+        ret = _get_default_directory()
+        assert ret == os.path.join('/tmp/fakehome', 'pre-commit')
 
 
 def test_uses_environment_variable_when_present():

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.9
requirements-dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aspy.yaml==1.3.0
cached-property==2.0.1
coverage==7.8.0
distlib==0.3.9
exceptiongroup==1.2.2
filelock==3.18.0
flake8==7.2.0
identify==2.6.9
iniconfig==2.1.0
mccabe==0.7.0
mock==5.2.0
nodeenv==1.9.1
packaging==24.2
platformdirs==4.3.7
pluggy==1.5.0
-e git+https://github.com/pre-commit/pre-commit.git@ef8347cf2dedfd818f7a170f8461131fb75223dc#egg=pre_commit
pycodestyle==2.13.0
pyflakes==3.3.1
pytest==8.3.5
pytest-env==1.1.5
PyYAML==6.0.2
six==1.17.0
tomli==2.2.1
virtualenv==20.29.3

name: pre-commit
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aspy-yaml==1.3.0
      - cached-property==2.0.1
      - coverage==7.8.0
      - distlib==0.3.9
      - exceptiongroup==1.2.2
      - filelock==3.18.0
      - flake8==7.2.0
      - identify==2.6.9
      - iniconfig==2.1.0
      - mccabe==0.7.0
      - mock==5.2.0
      - nodeenv==1.9.1
      - packaging==24.2
      - platformdirs==4.3.7
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.1
      - pytest==8.3.5
      - pytest-env==1.1.5
      - pyyaml==6.0.2
      - six==1.17.0
      - tomli==2.2.1
      - virtualenv==20.29.3
prefix: /opt/conda/envs/pre-commit

tests/commands/clean_test.py::test_clean tests/error_handler_test.py::test_log_and_exit tests/error_handler_test.py::test_error_handler_no_tty tests/main_test.py::test_expected_fatal_error_no_git_repo tests/store_test.py::test_get_default_directory_defaults_to_home tests/store_test.py::test_adheres_to_xdg_specification
tests/commands/clean_test.py::test_clean_empty tests/error_handler_test.py::test_error_handler_no_exception tests/error_handler_test.py::test_error_handler_fatal_error tests/error_handler_test.py::test_error_handler_uncaught_error tests/error_handler_test.py::test_error_handler_non_ascii_exception tests/main_test.py::test_overall_help tests/main_test.py::test_help_command tests/main_test.py::test_help_other_command tests/main_test.py::test_install_command[autoupdate] tests/main_test.py::test_install_command[clean] tests/main_test.py::test_install_command[install] tests/main_test.py::test_install_command[install-hooks] tests/main_test.py::test_install_command[run] tests/main_test.py::test_install_command[sample-config] tests/main_test.py::test_install_command[uninstall] tests/main_test.py::test_help_cmd_in_empty_directory tests/main_test.py::test_warning_on_tags_only tests/store_test.py::test_our_session_fixture_works tests/store_test.py::test_uses_environment_variable_when_present tests/store_test.py::test_store_require_created tests/store_test.py::test_store_require_created_does_not_create_twice tests/store_test.py::test_does_not_recreate_if_directory_already_exists tests/store_test.py::test_clone tests/store_test.py::test_clone_cleans_up_on_checkout_failure tests/store_test.py::test_clone_when_repo_already_exists tests/store_test.py::test_require_created_when_directory_exists_but_not_db
pre_commit/error_handler.py .travis.yml pre_commit/commands/clean.py appveyor.yml pre_commit/store.py
pre_commit/error_handler.py .travis.yml pre_commit/commands/clean.py appveyor.yml pre_commit/store.py