asottile: @Lucas-C look correct?
coveralls: 
[![Coverage Status](https://coveralls.io/builds/2591860/badge)](https://coveralls.io/builds/2591860)

Coverage decreased (-0.04%) to 99.96% when pulling **b140f92cd7e20368b27d19ea01227402e71c294a on no_defaults_in_config_227** into **9515ca06378d74f1e4f8013db2b5230c1f15edaa on master**.

diff --git a/pre_commit/clientlib/validate_config.py b/pre_commit/clientlib/validate_config.py
index bdd0e2c..e4a90a6 100644
--- a/pre_commit/clientlib/validate_config.py
+++ b/pre_commit/clientlib/validate_config.py
@@ -33,7 +33,7 @@ CONFIG_JSON_SCHEMA = {
                     'properties': {
                         'id': {'type': 'string'},
                         'files': {'type': 'string'},
-                        'exclude': {'type': 'string', 'default': '^$'},
+                        'exclude': {'type': 'string'},
                         'language_version': {'type': 'string'},
                         'args': {
                             'type': 'array',
@@ -71,7 +71,7 @@ def validate_config_extra(config):
             )
         for hook in repo['hooks']:
             try_regex(repo, hook['id'], hook.get('files', ''), 'files')
-            try_regex(repo, hook['id'], hook['exclude'], 'exclude')
+            try_regex(repo, hook['id'], hook.get('exclude', ''), 'exclude')
 
 
 load_config = get_validator(
diff --git a/pre_commit/clientlib/validate_manifest.py b/pre_commit/clientlib/validate_manifest.py
index 283d7c4..4295014 100644
--- a/pre_commit/clientlib/validate_manifest.py
+++ b/pre_commit/clientlib/validate_manifest.py
@@ -20,6 +20,7 @@ MANIFEST_JSON_SCHEMA = {
             'name': {'type': 'string'},
             'description': {'type': 'string', 'default': ''},
             'entry': {'type': 'string'},
+            'exclude': {'type': 'string', 'default': '^$'},
             'language': {'type': 'string'},
             'language_version': {'type': 'string', 'default': 'default'},
             'files': {'type': 'string'},
@@ -52,8 +53,14 @@ def validate_files(hook_config):
     if not is_regex_valid(hook_config['files']):
         raise InvalidManifestError(
             'Invalid files regex at {0}: {1}'.format(
-                hook_config['id'],
-                hook_config['files'],
+                hook_config['id'], hook_config['files'],
+            )
+        )
+
+    if not is_regex_valid(hook_config.get('exclude', '')):
+        raise InvalidManifestError(
+            'Invalid exclude regex at {0}: {1}'.format(
+                hook_config['id'], hook_config['exclude'],
             )
         )
 

Bug: base manifest value for 'exclude' is always ignored
I stumbled upon this bug while working on #226: the culprit is [`Repository.hooks`](https://github.com/pre-commit/pre-commit/blob/master/pre_commit/repository.py#L48).

A quick fix for this would be to simply remove the default value from `pre_commit/clientlib/validate_config.py`, but the root cause is that any default value defined for a field in this file will make the corresponding manifest field useless.

Basically here is what happens in `Repository.hooks`:
- all the hooks defined in the current repository are enumerated
- at this stage, a `hook` is a dict closely matching the Yaml the config file content, **plus** default values for fields not defined in the Yaml but having a JSON schema 'default'
- when doing the dict merge, **every** (key,value) pair in `hook` overrides the corresponding manifest entry. This includes default config value like `exclude: '$^'` overriding a base manifest value like `exclude: '.bak$'`

Hence I suggest either adding a test ensuring there will never be any 'default' defined in `CONFIG_JSON_SCHEMA`, or improving the merge logic.
diff --git a/tests/clientlib/validate_config_test.py b/tests/clientlib/validate_config_test.py
index c507f28..b474f1b 100644
--- a/tests/clientlib/validate_config_test.py
+++ b/tests/clientlib/validate_config_test.py
@@ -174,3 +174,23 @@ def test_config_with_local_hooks_definition_passes(config_obj):
     jsonschema.validate(config_obj, CONFIG_JSON_SCHEMA)
     config = apply_defaults(config_obj, CONFIG_JSON_SCHEMA)
     validate_config_extra(config)
+
+
+def test_does_not_contain_defaults():
+    """Due to the way our merging works, if this schema has any defaults they
+    will clobber potentially useful values in the backing manifest. #227
+    """
+    to_process = [(CONFIG_JSON_SCHEMA, ())]
+    while to_process:
+        schema, route = to_process.pop()
+        # Check this value
+        if isinstance(schema, dict):
+            if 'default' in schema:
+                raise AssertionError(
+                    'Unexpected default in schema at {0}'.format(
+                        ' => '.join(route),
+                    )
+                )
+
+            for key, value in schema.items():
+                to_process.append((value, route + (key,)))
diff --git a/tests/clientlib/validate_manifest_test.py b/tests/clientlib/validate_manifest_test.py
index 5e5690e..937f432 100644
--- a/tests/clientlib/validate_manifest_test.py
+++ b/tests/clientlib/validate_manifest_test.py
@@ -46,6 +46,9 @@ def test_additional_manifest_check_passing(obj):
         [{'id': 'a', 'language': 'not a language', 'files': ''}],
         [{'id': 'a', 'language': 'python3', 'files': ''}],
         [{'id': 'a', 'language': 'python', 'files': 'invalid regex('}],
+        [{'id': 'a', 'language': 'not a language', 'files': ''}],
+        [{'id': 'a', 'language': 'python3', 'files': ''}],
+        [{'id': 'a', 'language': 'python', 'files': '', 'exclude': '('}],
     ),
 )
 def test_additional_manifest_failing(obj):
diff --git a/tests/manifest_test.py b/tests/manifest_test.py
index ba30d42..39ecc74 100644
--- a/tests/manifest_test.py
+++ b/tests/manifest_test.py
@@ -22,6 +22,7 @@ def test_manifest_contents(manifest):
         'args': [],
         'description': '',
         'entry': 'bin/hook.sh',
+        'exclude': '^$',
         'expected_return_value': 0,
         'files': '',
         'id': 'bash_hook',
@@ -36,6 +37,7 @@ def test_hooks(manifest):
         'args': [],
         'description': '',
         'entry': 'bin/hook.sh',
+        'exclude': '^$',
         'expected_return_value': 0,
         'files': '',
         'id': 'bash_hook',

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.5
requirements-dev.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aspy.yaml==1.3.0
astroid==1.3.2
attrs==22.2.0
cached-property==1.5.2
certifi==2021.5.30
coverage==6.2
distlib==0.3.9
filelock==3.4.1
flake8==5.0.4
importlib-metadata==4.8.3
importlib-resources==5.4.0
iniconfig==1.1.1
jsonschema==3.2.0
logilab-common==1.9.7
mccabe==0.7.0
mock==5.2.0
mypy-extensions==1.0.0
nodeenv==1.6.0
ordereddict==1.1
packaging==21.3
platformdirs==2.4.0
pluggy==1.0.0
-e git+https://github.com/pre-commit/pre-commit.git@9515ca06378d74f1e4f8013db2b5230c1f15edaa#egg=pre_commit
py==1.11.0
pycodestyle==2.9.1
pyflakes==2.5.0
pylint==1.3.1
pyparsing==3.1.4
pyrsistent==0.18.0
pytest==7.0.1
PyYAML==6.0.1
simplejson==3.20.1
six==1.17.0
tomli==1.2.3
typing_extensions==4.1.1
virtualenv==20.17.1
zipp==3.6.0

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
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - 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=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - argparse==1.4.0
      - aspy-yaml==1.3.0
      - astroid==1.3.2
      - attrs==22.2.0
      - cached-property==1.5.2
      - coverage==6.2
      - distlib==0.3.9
      - filelock==3.4.1
      - flake8==5.0.4
      - importlib-metadata==4.8.3
      - importlib-resources==5.4.0
      - iniconfig==1.1.1
      - jsonschema==3.2.0
      - logilab-common==1.9.7
      - mccabe==0.7.0
      - mock==5.2.0
      - mypy-extensions==1.0.0
      - nodeenv==1.6.0
      - ordereddict==1.1
      - packaging==21.3
      - platformdirs==2.4.0
      - pluggy==1.0.0
      - py==1.11.0
      - pycodestyle==2.9.1
      - pyflakes==2.5.0
      - pylint==1.3.1
      - pyparsing==3.1.4
      - pyrsistent==0.18.0
      - pytest==7.0.1
      - pyyaml==6.0.1
      - simplejson==3.20.1
      - six==1.17.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - virtualenv==20.17.1
      - zipp==3.6.0
prefix: /opt/conda/envs/pre-commit

tests/clientlib/validate_config_test.py::test_does_not_contain_defaults tests/clientlib/validate_manifest_test.py::test_additional_manifest_failing[obj5]
tests/clientlib/validate_config_test.py::test_run[input0-0] tests/clientlib/validate_config_test.py::test_run[input1-0] tests/clientlib/validate_manifest_test.py::test_run[input0-0] tests/clientlib/validate_manifest_test.py::test_run[input1-0] tests/manifest_test.py::test_manifest_contents tests/manifest_test.py::test_hooks
tests/clientlib/validate_config_test.py::test_run[input2-1] tests/clientlib/validate_config_test.py::test_run[input3-1] tests/clientlib/validate_config_test.py::test_run[input4-1] tests/clientlib/validate_config_test.py::test_is_valid_according_to_schema[config_obj0-False] tests/clientlib/validate_config_test.py::test_is_valid_according_to_schema[config_obj1-True] tests/clientlib/validate_config_test.py::test_is_valid_according_to_schema[config_obj2-True] tests/clientlib/validate_config_test.py::test_is_valid_according_to_schema[config_obj3-False] tests/clientlib/validate_config_test.py::test_config_with_failing_regexes_fails tests/clientlib/validate_config_test.py::test_config_with_ok_regexes_passes tests/clientlib/validate_config_test.py::test_config_with_invalid_exclude_regex_fails tests/clientlib/validate_config_test.py::test_config_with_ok_exclude_regex_passes tests/clientlib/validate_config_test.py::test_config_with_local_hooks_definition_fails[config_obj0] tests/clientlib/validate_config_test.py::test_config_with_local_hooks_definition_passes[config_obj0] tests/clientlib/validate_config_test.py::test_config_with_local_hooks_definition_passes[config_obj1] tests/clientlib/validate_manifest_test.py::test_run[input2-1] tests/clientlib/validate_manifest_test.py::test_run[input3-1] tests/clientlib/validate_manifest_test.py::test_run[input4-1] tests/clientlib/validate_manifest_test.py::test_additional_manifest_check_raises_for_bad_language tests/clientlib/validate_manifest_test.py::test_additional_manifest_check_passing[obj0] tests/clientlib/validate_manifest_test.py::test_additional_manifest_check_passing[obj1] tests/clientlib/validate_manifest_test.py::test_additional_manifest_failing[obj0] tests/clientlib/validate_manifest_test.py::test_additional_manifest_failing[obj1] tests/clientlib/validate_manifest_test.py::test_additional_manifest_failing[obj2] tests/clientlib/validate_manifest_test.py::test_additional_manifest_failing[obj3] tests/clientlib/validate_manifest_test.py::test_additional_manifest_failing[obj4] tests/clientlib/validate_manifest_test.py::test_is_valid_according_to_schema[manifest_obj0-False] tests/clientlib/validate_manifest_test.py::test_is_valid_according_to_schema[manifest_obj1-True] tests/clientlib/validate_manifest_test.py::test_is_valid_according_to_schema[manifest_obj2-True]
pre_commit/clientlib/validate_manifest.py pre_commit/clientlib/validate_config.py
pre_commit/clientlib/validate_manifest.py pre_commit/clientlib/validate_config.py