Repository: pre-commit/pre-commit

Issue:
Question: always_skip unless called explicitly
Is there a way to add a hook to `.pre-commit-config.yaml` but have it always skip unless it's called explicitly? Granted, this is a weird use-case where I'm basically just trying to benefit from `pre-commit`'s package management abilities :)
So for example, while doing `git commit` or `pre-commit run` it would be marked as `Skipped` but would run when called like this:
```
pre-commit run <hook>
```

For context, I'm trying to simplify setting up [prmd](https://github.com/interagent/prmd) (a Ruby package) on Python repos. This already works (as part of a script)
`pre-commit run --verbose prmd --files <json>`
where
```
-   repo: /path/to/mirrors-prmd
    sha: v0.11.7
    hooks:
    -   id: prmd
        entry: prmd render
        args:
        - --template=main/api/doc/templates
```
but now I would like to prevent this hook from running in all other instances :)
As I said, weird use-case... maybe I just need to go to sleep now 😴 


Discussion:
Totally get this usecase 😆 

I think there should be a better way to do this but I'll tell you how I currently do this and give some additional ideas and then a feature idea :)

### abusing hook stages

Note: I personally don't use pre-push so if I don't want a hook to run normally I configure it as follows:

```yaml
    -   id: dont-run-normally
        stages: [push]
```

That way during `commit` it doesn't normally run.  However when I want to manually run it I can do: `pre-commit run dont-run-normally --hook-stage push` to make it run.

(woops! turns out `--hook-stage` is undocumented on [pre-commit.com](https://pre-commit.com/#pre-commit-run)!)

### try-repo

Another idea is to use [`pre-commit try-repo /path/to/repo`](https://pre-commit.com/#pre-commit-try-repo), probably not the best when you have to remember the incantation each time though :)

### rubyvenv

For ruby specifically, I've been slowly (haven't really made progress in a long time) been trying to factor out the pre-commit ruby support into a separate package (also so I can attempt to tackle #201 (the oldest as-of-now open issue)).  There's an incomplete, but for a lot of usecases works fine "ruby virtualenv"ish tool: https://github.com/asottile/rubyvenv -- it needs a lot of polishing, needs to support _building_ ruby, and still needs windows support somehow but otherwise is functional with prebuilt rubies.

### how I think this should work -- abusing hook stages for greater good!

I'd like to make the following work, does this sound reasonable?

the config:
```yaml
    -   id: dont-run-normally
        stages: [manual]
```

the invocation: `pre-commit run dont-run-normally --hook-stage manual`

thoughts?
Thanks @asottile !
I like the idea of `stages: [manual]` 💯 

Resolving patch:
diff --git a/pre_commit/clientlib.py b/pre_commit/clientlib.py
--- a/pre_commit/clientlib.py
+++ b/pre_commit/clientlib.py
@@ -35,10 +35,7 @@ def _make_argparser(filenames_help):
     cfgv.Required('id', cfgv.check_string),
     cfgv.Required('name', cfgv.check_string),
     cfgv.Required('entry', cfgv.check_string),
-    cfgv.Required(
-        'language',
-        cfgv.check_and(cfgv.check_string, cfgv.check_one_of(all_languages)),
-    ),
+    cfgv.Required('language', cfgv.check_one_of(all_languages)),
 
     cfgv.Optional(
         'files', cfgv.check_and(cfgv.check_string, cfgv.check_regex), '',
@@ -59,7 +56,7 @@ def _make_argparser(filenames_help):
     cfgv.Optional('language_version', cfgv.check_string, 'default'),
     cfgv.Optional('log_file', cfgv.check_string, ''),
     cfgv.Optional('minimum_pre_commit_version', cfgv.check_string, '0'),
-    cfgv.Optional('stages', cfgv.check_array(cfgv.check_string), []),
+    cfgv.Optional('stages', cfgv.check_array(cfgv.check_one_of(C.STAGES)), []),
     cfgv.Optional('verbose', cfgv.check_bool, False),
 )
 MANIFEST_SCHEMA = cfgv.Array(MANIFEST_HOOK_DICT)
diff --git a/pre_commit/constants.py b/pre_commit/constants.py
--- a/pre_commit/constants.py
+++ b/pre_commit/constants.py
@@ -20,3 +20,6 @@
 
 VERSION = pkg_resources.get_distribution('pre-commit').version
 VERSION_PARSED = pkg_resources.parse_version(VERSION)
+
+# `manual` is not invoked by any installed git hook.  See #719
+STAGES = ('commit', 'commit-msg', 'manual', 'push')
diff --git a/pre_commit/main.py b/pre_commit/main.py
--- a/pre_commit/main.py
+++ b/pre_commit/main.py
@@ -70,9 +70,8 @@ def _add_run_options(parser):
         help='Filename to check when running during `commit-msg`',
     )
     parser.add_argument(
-        '--hook-stage', choices=('commit', 'push', 'commit-msg'),
-        default='commit',
-        help='The stage during which the hook is fired e.g. commit or push.',
+        '--hook-stage', choices=C.STAGES, default='commit',
+        help='The stage during which the hook is fired.  One of %(choices)s',
     )
     parser.add_argument(
         '--show-diff-on-failure', action='store_true',


Test patch:
diff --git a/tests/commands/run_test.py b/tests/commands/run_test.py
--- a/tests/commands/run_test.py
+++ b/tests/commands/run_test.py
@@ -529,52 +529,37 @@ def test_lots_of_files(mock_out_store_directory, tempdir_factory):
         )
 
 
-def test_push_hook(cap_out, repo_with_passing_hook, mock_out_store_directory):
+def test_stages(cap_out, repo_with_passing_hook, mock_out_store_directory):
     config = OrderedDict((
         ('repo', 'local'),
         (
-            'hooks', (
-                OrderedDict((
-                    ('id', 'flake8'),
-                    ('name', 'hook 1'),
-                    ('entry', "'{}' -m flake8".format(sys.executable)),
-                    ('language', 'system'),
-                    ('types', ['python']),
-                    ('stages', ['commit']),
-                )),
-                OrderedDict((
-                    ('id', 'do_not_commit'),
-                    ('name', 'hook 2'),
-                    ('entry', 'DO NOT COMMIT'),
-                    ('language', 'pygrep'),
-                    ('types', ['text']),
-                    ('stages', ['push']),
-                )),
+            'hooks', tuple(
+                {
+                    'id': 'do-not-commit-{}'.format(i),
+                    'name': 'hook {}'.format(i),
+                    'entry': 'DO NOT COMMIT',
+                    'language': 'pygrep',
+                    'stages': [stage],
+                }
+                for i, stage in enumerate(('commit', 'push', 'manual'), 1)
             ),
         ),
     ))
     add_config_to_repo(repo_with_passing_hook, config)
 
-    open('dummy.py', 'a').close()
-    cmd_output('git', 'add', 'dummy.py')
-
-    _test_run(
-        cap_out,
-        repo_with_passing_hook,
-        {'hook_stage': 'commit'},
-        expected_outputs=[b'hook 1'],
-        expected_ret=0,
-        stage=False,
-    )
+    stage_a_file()
 
-    _test_run(
-        cap_out,
-        repo_with_passing_hook,
-        {'hook_stage': 'push'},
-        expected_outputs=[b'hook 2'],
-        expected_ret=0,
-        stage=False,
-    )
+    def _run_for_stage(stage):
+        args = run_opts(hook_stage=stage)
+        ret, printed = _do_run(cap_out, repo_with_passing_hook, args)
+        assert not ret, (ret, printed)
+        # this test should only run one hook
+        assert printed.count(b'hook ') == 1
+        return printed
+
+    assert _run_for_stage('commit').startswith(b'hook 1...')
+    assert _run_for_stage('push').startswith(b'hook 2...')
+    assert _run_for_stage('manual').startswith(b'hook 3...')
 
 
 def test_commit_msg_hook(cap_out, commit_msg_repo, mock_out_store_directory):
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -180,7 +180,8 @@ def __init__(self, stream):
     def get_bytes(self):
         """Get the output as-if no encoding occurred"""
         data = self._stream.data.getvalue()
-        self._stream.data.truncate(0)
+        self._stream.data.seek(0)
+        self._stream.data.truncate()
         return data
 
     def get(self):