{"id":50864249,"url":"https://github.com/gsaini/git-config-hooks-demo","last_synced_at":"2026-06-14T23:34:29.879Z","repository":{"id":352978506,"uuid":"1216901024","full_name":"gsaini/git-config-hooks-demo","owner":"gsaini","description":"Narrated, sandboxed, 60-second walkthrough of Git 2.54 config-based hooks. Nine color-coded scenes across two fake repos. One command — ./run.sh — and your real ~/.gitconfig is never touched.","archived":false,"fork":false,"pushed_at":"2026-04-21T23:29:13.000Z","size":20,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T23:34:29.164Z","etag":null,"topics":["git","git-config-hooks"],"latest_commit_sha":null,"homepage":"","language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gsaini.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-21T10:48:04.000Z","updated_at":"2026-04-21T23:27:37.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/gsaini/git-config-hooks-demo","commit_stats":null,"previous_names":["gsaini/git-config-hooks-demo"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/gsaini/git-config-hooks-demo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gsaini%2Fgit-config-hooks-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gsaini%2Fgit-config-hooks-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gsaini%2Fgit-config-hooks-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gsaini%2Fgit-config-hooks-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gsaini","download_url":"https://codeload.github.com/gsaini/git-config-hooks-demo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gsaini%2Fgit-config-hooks-demo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34342089,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["git","git-config-hooks"],"created_at":"2026-06-14T23:34:29.228Z","updated_at":"2026-06-14T23:34:29.848Z","avatar_url":"https://github.com/gsaini.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# git-config-hooks-demo\n\n![Bash](https://img.shields.io/badge/Bash-4EAA25?style=for-the-badge\u0026logo=gnu-bash\u0026logoColor=white)\n![Git](https://img.shields.io/badge/Git%202.54+-F05032?style=for-the-badge\u0026logo=git\u0026logoColor=white)\n![macOS](https://img.shields.io/badge/macOS-000000?style=for-the-badge\u0026logo=apple\u0026logoColor=white)\n![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge\u0026logo=linux\u0026logoColor=black)\n![License: MIT](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)\n\nA **narrated, sandboxed, 60-second walkthrough** of Git 2.54's new config-based\nhooks. Run one command, watch a color-coded story play out across two fake\nrepos, understand the feature end-to-end.\n\n\u003e Production-ready counterpart: **[git-config-hooks-policy](https://github.com/gsaini/git-config-hooks-policy)**\n\u003e — the hook bundle this demo exercises, packaged for real-world rollout.\n\n---\n\n## What you'll see\n\nNine scenes, ~45 seconds at default pacing:\n\n1. **Meet the Policy Bundle** — four hook scripts, one config fragment. Nothing else.\n2. **Install** — `[include]` the bundle from a sandboxed `$GIT_CONFIG_GLOBAL`.\n3. **Two fresh repos — zero per-repo hook setup.** Both inherit everything.\n4. **Catch a leaked credential** — AWS key + RSA key in a config file. Two hooks reject in parallel; you get every finding at once, not one-at-a-time.\n5. **Fix the leak; see the branch guard in isolation** — now only `no-direct-main` speaks up. Clean signal.\n6. **Topic branch; message guard in isolation; clean commit lands.** All three pre-commit hooks + commit-msg run silently on success.\n7. **Per-repo opt-out** — `hook.no-direct-main.enabled = false` in the other repo. Config preserved, hook skipped. `--show-scope` prints a `disabled` badge.\n8. **Scopes compose. Events compose.** A local-scope hook layered on top of global. One hook wired to two events (`pre-commit` + `pre-push`).\n9. **Policy coverage report** — active hooks across both repos, plus the four-step org rollout recipe.\n\nEvery red `✗` you see is a hook correctly blocking a bad action. Every green\n`✓` is a hook passing — silently, the way they should in a healthy repo.\n\n---\n\n## Visual overview\n\n### 1. How the Policy Bundle plugs in\n\nOne `include.path` line pulls the bundle into the user's (or system's) git\nconfig. Each `[hook \"...\"]` stanza registers one hook against one-or-more\nevents. No repo-local setup, no `core.hooksPath`.\n\n```text\n  ~/.gitconfig  ──include.path──▶  policy.gitconfig\n                                          │\n                ┌─────────────────┬───────┴─────────┬────────────────────┐\n                ▼                 ▼                 ▼                    ▼\n         hook.secret-scan  hook.no-direct-main  hook.conv-commit  hook.tests-reminder\n                │                 │                 │                    │\n         ┌──────┴──────┐          │                 │                    │\n         ▼             ▼          ▼                 ▼                    ▼\n    [pre-commit]  [pre-push]  [pre-commit]    [commit-msg]          [pre-push]\n         │             │          │                 │                    │\n         └─────────────┴──────────┴─────────────────┴────────────────────┘\n                                  │\n                                  ▼\n                     every repo on the machine\n```\n\n### 2. What fires on `git commit`\n\nGit parses config in **system → global → local** order, builds a per-event\nchain, and runs each entry in discovery order. Traditional\n`.git/hooks/\u003cevent\u003e` scripts still run, last.\n\n```text\n  git commit -m \"feat(x): ...\"\n            │\n            ▼\n  ┌───────────────────────────────┐\n  │  parse config                 │\n  │  system → global → local      │\n  └───────────────┬───────────────┘\n                  │\n                  ▼\n  ┌───────────────────────────────┐\n  │  pre-commit chain             │\n  │    1. secret-scan.sh          │\n  │    2. no-direct-main.sh       │\n  │    3. .git/hooks/pre-commit   │  ◀── hookdir runs last\n  └───────────────┬───────────────┘\n                  │\n         any exits non-zero? ──── yes ──▶  ✗ commit blocked\n                  │\n                  no\n                  ▼\n  ┌───────────────────────────────┐\n  │  commit-msg chain             │\n  │    1. conventional-commit.sh  │\n  └───────────────┬───────────────┘\n                  │\n         subject valid? ──── no ──▶  ✗ commit blocked\n                  │\n                  yes\n                  ▼\n           ✓ commit created\n```\n\n### 3. The nine-scene demo journey\n\n```text\n  ┌───────────────────────────────────────────────────────────────────────┐\n  │  1 · Meet the Policy Bundle                 4 hooks, 1 fragment       │\n  └────────────────────────────────┬──────────────────────────────────────┘\n                                   ▼\n  ┌───────────────────────────────────────────────────────────────────────┐\n  │  2 · Install                                 [include] the bundle     │\n  └────────────────────────────────┬──────────────────────────────────────┘\n                                   ▼\n  ┌───────────────────────────────────────────────────────────────────────┐\n  │  3 · Two fresh repos                         zero per-repo setup      │\n  └────────────────────────────────┬──────────────────────────────────────┘\n                                   ▼\n  ┌───────────────────────────────────────────────────────────────────────┐\n  │  4 · Leaked credential      ✗ secret-scan + no-direct-main (parallel) │\n  └────────────────────────────────┬──────────────────────────────────────┘\n                                   ▼\n  ┌───────────────────────────────────────────────────────────────────────┐\n  │  5 · Fix leak                                ✗ no-direct-main alone   │\n  └────────────────────────────────┬──────────────────────────────────────┘\n                                   ▼\n  ┌───────────────────────────────────────────────────────────────────────┐\n  │  6 · Topic branch                            ✓ clean commit lands     │\n  └────────────────────────────────┬──────────────────────────────────────┘\n                                   ▼\n  ┌───────────────────────────────────────────────────────────────────────┐\n  │  7 · Per-repo opt-out              hook.\u003cname\u003e.enabled = false        │\n  └────────────────────────────────┬──────────────────────────────────────┘\n                                   ▼\n  ┌───────────────────────────────────────────────────────────────────────┐\n  │  8 · Scopes + events compose       local on top of global             │\n  └────────────────────────────────┬──────────────────────────────────────┘\n                                   ▼\n  ┌───────────────────────────────────────────────────────────────────────┐\n  │  9 · Coverage report                 + org rollout recipe             │\n  └───────────────────────────────────────────────────────────────────────┘\n```\n\n---\n\n## Requirements\n\n- **Git 2.54 or later.** `git --version` must report `2.54.0+`. Lower versions\n  don't recognize `[hook \"...\"]` configs or the `git hook` subcommand.\n- **Bash 3.2+, POSIX `grep`, `sed`.** Standard on macOS and every Linux distro.\n- **A terminal with ANSI color support** is recommended but not required.\n\n---\n\n## Run it\n\n```bash\ngit clone https://github.com/gsaini/git-config-hooks-demo.git\ncd git-config-hooks-demo\n./run.sh\n```\n\nThat's the whole thing.\n\n### Controls\n\n| Variable | Effect | Default |\n| --- | --- | --- |\n| `PAUSE=1` | Wait for Enter between scenes. Best for live presentations. | off |\n| `SPEED=N` | Seconds between beats when auto-pacing (`0` = instant). | `0.6` |\n\n```bash\nPAUSE=1 ./run.sh        # live demo, audience-paced\nSPEED=0 ./run.sh        # fast-forward through everything (good for CI smoke test)\nSPEED=2 ./run.sh        # slow cinema mode\n```\n\n### Reset\n\nThe demo writes only into `./sandbox/`. To re-run from scratch:\n\n```bash\n./reset.sh              # wipes ./sandbox/\n./run.sh\n```\n\n---\n\n## Your real git config is safe\n\nBefore any git command runs, the driver sets:\n\n```bash\nexport GIT_CONFIG_GLOBAL=./sandbox/fake-home/.gitconfig\nexport GIT_CONFIG_SYSTEM=/dev/null\n```\n\nEvery repo-scoped `git config` call reads and writes inside `./sandbox/` only.\nYour real `~/.gitconfig` and `/etc/gitconfig` are **never read, never mutated.**\nNuking `./sandbox/` is the total undo.\n\n---\n\n## The Policy Bundle (embedded in `policy/`)\n\nThe demo ships a vendored copy of the bundle, pinned to a known-good version\nso the narration stays accurate. In production you'd point git at the\ncanonical repo instead — see\n[git-config-hooks-policy](https://github.com/gsaini/git-config-hooks-policy)\nfor deployment docs.\n\nFour hooks, one config fragment:\n\n| Hook | Event(s) | Action |\n| --- | --- | --- |\n| `secret-scan` | `pre-commit`, `pre-push` | Block AWS keys, PEM keys, GitHub/Slack/OpenAI tokens, GCP service-account JSON. |\n| `no-direct-main` | `pre-commit` | Refuse commits on `main`/`master`/`trunk`/`production`/`release`. |\n| `conventional-commit` | `commit-msg` | Enforce `type(scope)!?: subject` with 72-char subject cap. |\n| `tests-reminder` | `pre-push` | Non-blocking nudge: tests? docs? PR description? |\n\nThe config fragment is ~25 lines of ini:\n\n```ini\n[hook \"secret-scan\"]\n    command = __POLICY_ROOT__/hooks/secret-scan.sh\n    event = pre-commit\n    event = pre-push\n\n[hook \"no-direct-main\"]\n    command = __POLICY_ROOT__/hooks/no-direct-main.sh\n    event = pre-commit\n\n[hook \"conventional-commit\"]\n    command = __POLICY_ROOT__/hooks/conventional-commit.sh\n    event = commit-msg\n\n[hook \"tests-reminder\"]\n    command = __POLICY_ROOT__/hooks/tests-reminder.sh\n    event = pre-push\n```\n\n`__POLICY_ROOT__` is substituted with an absolute path at setup time.\n\n---\n\n## Feature primer — what's new in Git 2.54\n\nBefore 2.54 you had **one** per-event file in `.git/hooks/`, shareable only via\n`core.hooksPath` (all-or-nothing) or third-party tools like husky/pre-commit.\nComposing multiple org-wide checks required a wrapper script you maintained\nyourself.\n\nGit 2.54 adds a config-file-native model:\n\n```ini\n[hook \"\u003cname\u003e\"]\n    command = \u003cpath or shell oneliner\u003e  # what to execute\n    event   = \u003chook-event\u003e              # multi-valued — same hook, many events\n    enabled = true|false                # opt out without deleting config\n```\n\nPlus two subcommands:\n\n- `git hook list --show-scope \u003cevent\u003e` — inventory of configured hooks, with\n  the scope (system / global / local) each came from, and a `disabled` badge\n  when turned off.\n- `git hook run \u003cevent\u003e [-- \u003cargs\u003e]` — invoke the hook chain manually\n  (useful for testing and for wrapper tools; see the\n  [`man git-hook`](https://git-scm.com/docs/git-hook) \"Wrappers\" section).\n\nExecution order: hooks fire in the order git encounters their config during\nparse (system → global → local, and within each file top-to-bottom).\nTraditional `.git/hooks/\u003cevent\u003e` scripts still work and run **last** — they\nshow up in `git hook list --show-scope` as `hook from hookdir`.\n\n\u003e **No trust model.** Config-based hooks execute with the current user's\n\u003e privileges, just like `.git/hooks/`. Don't source policy bundles from places\n\u003e an attacker can mutate. System-scope installation from a root-owned path is\n\u003e the defensible rollout pattern.\n\n---\n\n## Directory layout\n\n```text\ngit-config-hooks-demo/\n├── run.sh                       # the 9-scene narrated driver\n├── lib.sh                       # ANSI colors + scene/run/pause helpers\n├── reset.sh                     # wipes ./sandbox/\n├── policy/                      # vendored Policy Bundle\n│   ├── hooks/\n│   │   ├── secret-scan.sh\n│   │   ├── no-direct-main.sh\n│   │   ├── conventional-commit.sh\n│   │   └── tests-reminder.sh\n│   └── policy.gitconfig.tmpl    # [hook \"...\"] fragment with placeholder\n├── README.md                    # (this file)\n├── LICENSE\n└── .gitignore                   # ignores ./sandbox/ at the repo root\n#\n# created at runtime by run.sh, gitignored:\n#   sandbox/fake-home/.gitconfig\n#   sandbox/fake-home/policy.gitconfig\n#   sandbox/payments-service/       (a demo repo)\n#   sandbox/analytics-api/          (another demo repo)\n```\n\n---\n\n## Use this as a learning jumping-off point\n\nAfter the demo runs, poke at the sandbox directly:\n\n```bash\n# The config the demo installed:\ncat sandbox/fake-home/.gitconfig\ncat sandbox/fake-home/policy.gitconfig\n\n# Everything git sees from inside a demo repo:\ncd sandbox/payments-service\nGIT_CONFIG_GLOBAL=../fake-home/.gitconfig GIT_CONFIG_SYSTEM=/dev/null \\\n    git config --list --show-origin --show-scope\n\n# Run a hook chain by hand:\nGIT_CONFIG_GLOBAL=../fake-home/.gitconfig GIT_CONFIG_SYSTEM=/dev/null FORCE_COLOR=1 \\\n    git hook run pre-commit\n\n# Try writing your own hook, register it at local scope:\nGIT_CONFIG_GLOBAL=../fake-home/.gitconfig GIT_CONFIG_SYSTEM=/dev/null \\\n    git config set hook.my-thing.command /path/to/my-script.sh\nGIT_CONFIG_GLOBAL=../fake-home/.gitconfig GIT_CONFIG_SYSTEM=/dev/null \\\n    git config set --append hook.my-thing.event pre-commit\n```\n\n---\n\n## Related\n\n- **[git-config-hooks-policy](https://github.com/gsaini/git-config-hooks-policy)** — the bundle, packaged for production.\n- [Git 2.54 highlights (GitHub blog)](https://github.blog/open-source/git/highlights-from-git-2-54/)\n- [`man git-hook`](https://git-scm.com/docs/git-hook) — authoritative reference for the `hook.*` config schema and subcommands.\n- [`man githooks`](https://git-scm.com/docs/githooks) — complete list of hook events.\n\n---\n\n## License\n\nMIT — see [LICENSE](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgsaini%2Fgit-config-hooks-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgsaini%2Fgit-config-hooks-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgsaini%2Fgit-config-hooks-demo/lists"}