diff --git a/pre_commit/commands/hook_impl.py b/pre_commit/commands/hook_impl.py
index 4843fc7..d0e226f 100644
--- a/pre_commit/commands/hook_impl.py
+++ b/pre_commit/commands/hook_impl.py
@@ -150,6 +150,7 @@ def _pre_push_ns(
 _EXPECTED_ARG_LENGTH_BY_HOOK = {
     'commit-msg': 1,
     'post-checkout': 3,
+    'post-commit': 0,
     'pre-commit': 0,
     'pre-merge-commit': 0,
     'pre-push': 2,
@@ -186,7 +187,7 @@ def _run_ns(
         return _pre_push_ns(color, args, stdin)
     elif hook_type in {'commit-msg', 'prepare-commit-msg'}:
         return _ns(hook_type, color, commit_msg_filename=args[0])
-    elif hook_type in {'pre-merge-commit', 'pre-commit'}:
+    elif hook_type in {'post-commit', 'pre-merge-commit', 'pre-commit'}:
         return _ns(hook_type, color)
     elif hook_type == 'post-checkout':
         return _ns(
diff --git a/pre_commit/commands/run.py b/pre_commit/commands/run.py
index 8c8401c..8a9352d 100644
--- a/pre_commit/commands/run.py
+++ b/pre_commit/commands/run.py
@@ -221,7 +221,8 @@ def _compute_cols(hooks: Sequence[Hook]) -> int:
 
 
 def _all_filenames(args: argparse.Namespace) -> Collection[str]:
-    if args.hook_stage == 'post-checkout':  # no files for post-checkout
+    # these hooks do not operate on files
+    if args.hook_stage in {'post-checkout', 'post-commit'}:
         return ()
     elif args.hook_stage in {'prepare-commit-msg', 'commit-msg'}:
         return (args.commit_msg_filename,)
diff --git a/pre_commit/constants.py b/pre_commit/constants.py
index e2b8e3a..5150fdc 100644
--- a/pre_commit/constants.py
+++ b/pre_commit/constants.py
@@ -17,8 +17,8 @@ VERSION = importlib_metadata.version('pre_commit')
 
 # `manual` is not invoked by any installed git hook.  See #719
 STAGES = (
-    'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', 'manual',
-    'post-checkout', 'push',
+    'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg',
+    'post-commit', 'manual', 'post-checkout', 'push',
 )
 
 DEFAULT = 'default'
diff --git a/pre_commit/main.py b/pre_commit/main.py
index 790b347..874eb53 100644
--- a/pre_commit/main.py
+++ b/pre_commit/main.py
@@ -79,7 +79,7 @@ def _add_hook_type_option(parser: argparse.ArgumentParser) -> None:
     parser.add_argument(
         '-t', '--hook-type', choices=(
             'pre-commit', 'pre-merge-commit', 'pre-push',
-            'prepare-commit-msg', 'commit-msg', 'post-checkout',
+            'prepare-commit-msg', 'commit-msg', 'post-commit', 'post-checkout',
         ),
         action=AppendReplaceDefault,
         default=['pre-commit'],

diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py
index ddf65b7..cce4a25 100644
--- a/tests/commands/hook_impl_test.py
+++ b/tests/commands/hook_impl_test.py
@@ -96,6 +96,7 @@ def test_run_legacy_recursive(tmpdir):
         ('pre-merge-commit', []),
         ('pre-push', ['branch_name', 'remote_name']),
         ('commit-msg', ['.git/COMMIT_EDITMSG']),
+        ('post-commit', []),
         ('post-checkout', ['old_head', 'new_head', '1']),
         # multiple choices for commit-editmsg
         ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']),
@@ -149,6 +150,13 @@ def test_run_ns_commit_msg():
     assert ns.commit_msg_filename == '.git/COMMIT_MSG'
 
 
+def test_run_ns_post_commit():
+    ns = hook_impl._run_ns('post-commit', True, (), b'')
+    assert ns is not None
+    assert ns.hook_stage == 'post-commit'
+    assert ns.color is True
+
+
 def test_run_ns_post_checkout():
     ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'')
     assert ns is not None
diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py
index 66b9190..6d75e68 100644
--- a/tests/commands/install_uninstall_test.py
+++ b/tests/commands/install_uninstall_test.py
@@ -726,6 +726,32 @@ def test_commit_msg_legacy(commit_msg_repo, tempdir_factory, store):
     assert second_line.startswith('Must have "Signed off by:"...')
 
 
+def test_post_commit_integration(tempdir_factory, store):
+    path = git_dir(tempdir_factory)
+    config = [
+        {
+            'repo': 'local',
+            'hooks': [{
+                'id': 'post-commit',
+                'name': 'Post commit',
+                'entry': 'touch post-commit.tmp',
+                'language': 'system',
+                'always_run': True,
+                'verbose': True,
+                'stages': ['post-commit'],
+            }],
+        },
+    ]
+    write_config(path, config)
+    with cwd(path):
+        _get_commit_output(tempdir_factory)
+        assert not os.path.exists('post-commit.tmp')
+
+        install(C.CONFIG_FILE, store, hook_types=['post-commit'])
+        _get_commit_output(tempdir_factory)
+        assert os.path.exists('post-commit.tmp')
+
+
 def test_post_checkout_integration(tempdir_factory, store):
     path = git_dir(tempdir_factory)
     config = [
diff --git a/tests/repository_test.py b/tests/repository_test.py
index 3c7a637..f55c34c 100644
--- a/tests/repository_test.py
+++ b/tests/repository_test.py
@@ -880,7 +880,7 @@ def test_manifest_hooks(tempdir_factory, store):
         require_serial=False,
         stages=(
             'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg',
-            'manual', 'post-checkout', 'push',
+            'post-commit', 'manual', 'post-checkout', 'push',
         ),
         types=['file'],
         verbose=False,

allow post-commit hook
I would like to tag git versions whenever specific conditions are true. 

I can't use pre-commit because I don't have a commit yet, pre-push would lead to the new tag not being pushed together with the rest. Did I miss something or is this hook simply not added yet?
tests/commands/hook_impl_test.py::test_check_args_length_ok[post-commit-args4] tests/commands/hook_impl_test.py::test_run_ns_post_commit
tests/commands/hook_impl_test.py::test_validate_config_file_exists tests/commands/hook_impl_test.py::test_validate_config_missing tests/commands/hook_impl_test.py::test_validate_config_skip_missing_config tests/commands/hook_impl_test.py::test_validate_config_skip_via_env_variable tests/commands/hook_impl_test.py::test_run_legacy_does_not_exist tests/commands/hook_impl_test.py::test_run_legacy_executes_legacy_script tests/commands/hook_impl_test.py::test_run_legacy_pre_push_returns_stdin tests/commands/hook_impl_test.py::test_run_legacy_recursive tests/commands/hook_impl_test.py::test_check_args_length_ok[pre-commit-args0] tests/commands/hook_impl_test.py::test_check_args_length_ok[pre-merge-commit-args1] tests/commands/hook_impl_test.py::test_check_args_length_ok[pre-push-args2] tests/commands/hook_impl_test.py::test_check_args_length_ok[commit-msg-args3] tests/commands/hook_impl_test.py::test_check_args_length_ok[post-checkout-args5] tests/commands/hook_impl_test.py::test_check_args_length_ok[prepare-commit-msg-args6] tests/commands/hook_impl_test.py::test_check_args_length_ok[prepare-commit-msg-args7] tests/commands/hook_impl_test.py::test_check_args_length_ok[prepare-commit-msg-args8] tests/commands/hook_impl_test.py::test_check_args_length_error_too_many_plural tests/commands/hook_impl_test.py::test_check_args_length_error_too_many_singluar tests/commands/hook_impl_test.py::test_check_args_length_prepare_commit_msg_error tests/commands/hook_impl_test.py::test_run_ns_pre_commit tests/commands/hook_impl_test.py::test_run_ns_commit_msg tests/commands/hook_impl_test.py::test_run_ns_post_checkout tests/commands/hook_impl_test.py::test_hook_impl_main_runs_hooks tests/commands/install_uninstall_test.py::test_is_not_script tests/commands/install_uninstall_test.py::test_is_script tests/commands/install_uninstall_test.py::test_is_previous_pre_commit tests/commands/install_uninstall_test.py::test_shebang_windows tests/commands/install_uninstall_test.py::test_shebang_posix_not_on_path tests/commands/install_uninstall_test.py::test_shebang_posix_on_path tests/commands/install_uninstall_test.py::test_install_pre_commit tests/commands/install_uninstall_test.py::test_install_hooks_directory_not_present tests/commands/install_uninstall_test.py::test_install_multiple_hooks_at_once tests/commands/install_uninstall_test.py::test_install_refuses_core_hookspath tests/commands/install_uninstall_test.py::test_install_hooks_dead_symlink tests/commands/install_uninstall_test.py::test_uninstall_does_not_blow_up_when_not_there tests/commands/install_uninstall_test.py::test_uninstall tests/commands/install_uninstall_test.py::test_uninstall_doesnt_remove_not_our_hooks tests/repository_test.py::test_grep_hook_matching tests/repository_test.py::test_grep_hook_case_insensitive tests/repository_test.py::test_grep_hook_not_matching[nope] tests/repository_test.py::test_grep_hook_not_matching[foo'bar] tests/repository_test.py::test_grep_hook_not_matching[^\\[INFO\\]] tests/repository_test.py::test_fail_hooks tests/repository_test.py::test_unknown_keys tests/repository_test.py::test_default_language_version tests/repository_test.py::test_default_stages