• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sds / overcommit / 42767965538

10 Nov 2026 06:21AM UTC coverage: 92.847% (+0.3%) from 92.522%
42767965538

push

github

web-flow
Remember all previously approved configurations and hooks (#665)

Checking out or rebasing through an older, already approved configuration currently fails signature verification because signing a newer version overwrites the only stored approval. Remember every locally approved configuration and hook version, while continuing to reject unseen changes and unapproved combinations of hook code and configuration.

Fixes #300.

Approvals now live in `overcommit-signatures` under the repository's Git directory. Each file is indexed by the SHA256 of its signature config key and its existing SHA256 signature, and contains both values for inspection. Lookup reads one fixed-size file regardless of history length. History has no cap and does not grow `.git/config` or slow Git config reads.

This replaces the original PR's Git blob design and addresses the security concerns in its review:

- Approvals are local filesystem data outside the Git object database. Fetching attacker-controlled commits, blobs, tags, or other refs cannot grant trust.
- Git garbage collection cannot remove approvals.
- Revoke one approval by deleting its file; revoke all approvals by deleting `overcommit-signatures`. No refs, keys, or cryptographic trust infrastructure are required.
- Existing Git config signatures migrate on first use without changing their digest format. The legacy key is removed only after its approval is safely written, so deleting history does not resurrect an old approval.
- Linked worktrees share history by default. With `extensions.worktreeConfig`, both history and the existing verification setting retain their per-worktree isolation.
- A hook's code and configuration remain signed together. Individually approved components do not approve new combinations.

README documents storage, migration, worktrees, and revocation. Older Overcommit releases do not read the new history and will require re-signing if used after migration.

Validation:

- Full suite: 1,785 exam... (continued)

53 of 60 new or added lines in 7 files covered. (88.33%)

3388 of 3649 relevant lines covered (92.85%)

2089.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

34.62
/lib/overcommit/hook_loader/plugin_hook_loader.rb
1
# frozen_string_literal: true
2

3
require 'digest'
84 ✔
4

5
module Overcommit::HookLoader
84 ✔
6
  # Responsible for loading hooks that are specific to the repository Overcommit
7
  # is running in.
8
  class PluginHookLoader < Base
84 ✔
9
    def load_hooks
84 ✔
10
      check_for_unverified_plugins if @config.verify_signatures?
6 ✔
11

12
      hooks = plugin_paths.map do |plugin_path|
6 ✔
13
        require plugin_path
×
14

15
        hook_name = Overcommit::Utils.camel_case(File.basename(plugin_path, '.rb'))
×
16
        create_hook(hook_name)
×
17
      end
18

19
      hooks + ad_hoc_hook_names.map do |hook_name|
6 ✔
20
        create_ad_hoc_hook(hook_name)
×
21
      end
22
    end
23

24
    def update_signatures
84 ✔
NEW
25
      log.success('All plugin signatures have been verified') if unverified_plugins.empty?
×
26

NEW
27
      unverified_plugins.each do |plugin|
×
28
        plugin.update_signature!
×
29
        log.warning "Updated signature of plugin #{plugin.hook_name}"
×
30
      end
31
    end
32

33
    private
84 ✔
34

35
    def plugin_paths
84 ✔
36
      directory = File.join(@config.plugin_directory, @context.hook_type_name)
6 ✔
37
      Dir[File.join(directory, '*.rb')].sort
6 ✔
38
    end
39

40
    def plugin_hook_names
84 ✔
41
      plugin_paths.map do |path|
×
42
        Overcommit::Utils.camel_case(File.basename(path, '.rb'))
×
43
      end
44
    end
45

46
    def ad_hoc_hook_names
84 ✔
47
      @config.enabled_ad_hoc_hooks(@context)
6 ✔
48
    end
49

50
    def unverified_plugins
84 ✔
51
      (plugin_hook_names + ad_hoc_hook_names).
×
52
        map { |hook_name| Overcommit::HookSigner.new(hook_name, @config, @context) }.
×
53
        reject(&:signature_verified?)
54
    end
55

56
    def check_for_unverified_plugins
84 ✔
NEW
57
      return if unverified_plugins.empty?
×
58

59
      log.bold_warning "The following #{@context.hook_script_name} plugins " \
×
60
                       'have been added, changed, or had their configuration modified:'
61
      log.newline
×
62

NEW
63
      unverified_plugins.each do |signer|
×
64
        log.warning " * #{signer.hook_name} in #{signer.hook_path}"
×
65
      end
66

67
      log.newline
×
68
      log.bold_warning 'You should verify the changes and then run:'
×
69
      log.newline
×
70
      log.warning "overcommit --sign #{@context.hook_script_name}"
×
71
      log.newline
×
72
      log.log "For more information, see #{Overcommit::REPO_URL}#security"
×
73

74
      raise Overcommit::Exceptions::InvalidHookSignature
×
75
    end
76

77
    def create_ad_hoc_hook(hook_name)
84 ✔
78
      hook_module = Overcommit::Hook.const_get(@context.hook_class_name)
×
79
      hook_base = hook_module.const_get('Base')
×
80

81
      # Implement a simple class that executes the command and returns pass/fail
82
      # based on the exit status
83
      hook_class = Class.new(hook_base) do
×
84
        def run
×
85
          result = @context.execute_hook(command)
×
86

87
          if result.success?
×
88
            :pass
×
89
          else
90
            [:fail, result.stdout + result.stderr]
×
91
          end
92
        end
93
      end
94

95
      hook_module.const_set(hook_name, hook_class).new(@config, @context)
×
96
    rescue LoadError, NameError => e
97
      raise Overcommit::Exceptions::HookLoadError,
×
98
            "Unable to load hook '#{hook_name}': #{e}",
99
            e.backtrace
100
    end
101
  end
102
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc