Skip to content

newell-ma/immutable-files

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

immutable-files

Enforce file immutability through layered git hooks. Mark a file with the literal text @immutable inside a native comment for its language, and every subsequent attempt to modify, rename, or delete that file is blocked.

Defence in depth

# Layer Where it runs Bypassable with
1 pre-commit hook Local (git commit) git commit --no-verify
2 pre-push hook Local (git push) git push --no-verify
3 GitHub Actions Server-side, on PR + push to main Repo admin disables CI
4 Branch protection GitHub setting on main Repo admin disables it

Each layer catches what the layer above it failed to. A developer who skips --no-verify twice still hits CI; CI cannot be bypassed without admin rights once branch protection requires it.

The marker

Put the literal text @immutable inside a comment in the file. The comment syntax is whatever is native to that file type, so the file remains valid:

File type Comment Example marker line
SQL -- -- @immutable
C#, Java, JS, TS, Go, C, C++, Rust // // @immutable
Python, Shell, PowerShell, YAML, TOML # # @immutable
HTML, XML, Markdown <!-- --> <!-- @immutable -->
CSS /* */ /* @immutable */

The hooks do not parse comment syntax; they just look for the literal string @immutable in the file's stored blob. Putting it in a comment is what keeps your file syntactically valid.

First-commit rule: a file may be added with the marker. Once committed, future Modify / Delete / Rename operations are blocked.

One-time setup (per clone)

core.hooksPath is a per-clone setting; new clones don't pick it up automatically. Each contributor runs the bootstrap once:

# bash / WSL / macOS / Linux
./scripts/enable-hooks.sh
# Windows / PowerShell
./scripts/enable-hooks.ps1

Verify:

git config core.hooksPath
# .githooks

How it triggers

Try to edit samples/locked.sql and commit:

echo "-- evil change" >> samples/locked.sql
git add samples/locked.sql
git commit -m "tamper"
# Commit blocked: immutable file(s) modified.
#   M  samples/locked.sql

Legitimate updates to an immutable file

Two commits are required, and reviewers should treat the first one as the audit gate:

  1. Remove the @immutable marker. Commit with a message that explains why the file is no longer immutable. CI will allow this because the post-change blob no longer matches future immutability checks — but the pre-change blob did, so the hook will fire. This commit is the one that must be authorised.

    In practice this means: either land it via a reviewed PR while CI is your enforcement (recommended), or use --no-verify locally with explicit justification recorded in the commit body, then push and let CI + reviewers be the gate.

  2. Make the actual change in a second commit.

Re-add the marker afterwards if the file should become immutable again.

GitHub setup (when you push this to GitHub)

  1. Push the repo and open a PR to main. Confirm the Immutable file check workflow runs.
  2. Settings → Branches → Add classic branch protection rule (or Rulesets) for main:
    • Require a pull request before merging.
    • Require status checks to pass before merging → select Reject changes to @immutable files.
    • Do not allow bypassing the above settings.
    • (Optional) Require signed commits.
  3. (Optional) Add a CODEOWNERS entry covering immutable files so a human reviewer must approve any PR that removes a marker.

Customising the marker

Set IMMUTABLE_MARKER in the environment (hooks honour it; you'd also need to mirror it into the workflow env: block):

IMMUTABLE_MARKER='@locked' git commit -m "..."

Layout

.githooks/              # in-repo hooks (referenced via core.hooksPath)
  _lib.sh
  pre-commit
  pre-push
.github/workflows/
  immutable-check.yml   # server-side enforcement
scripts/
  enable-hooks.sh
  enable-hooks.ps1
samples/                # demo files with native-comment markers
  locked.sql
  Locked.cs
  locked.ps1
  locked.html
  editable.sql          # control: not marked, edits allowed

Known limitations

  • git commit --no-verify / git push --no-verify bypass layers 1 and 2. That is exactly why layer 3 (CI + branch protection) exists.
  • Hooks only run on local clones that have run enable-hooks.sh. Treat layers 1–2 as ergonomics; layer 3 is the real enforcement.
  • A force-push that rewrites history to remove the marker before the change can defeat CI on push-to-main. Branch protection should forbid force pushes to main.

About

Layered git-hook enforcement of file immutability via an @immutable comment marker

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors