Skip to content

tibberous/git-symlinks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

git-symlinks

Make git follow symbolic links — opt-in, with a clear warning before the first real action — without forking git, without patching git, and without breaking across git releases.

┌─────────────────────────────────────────────────────────────────────┐
│ WARNING: this repo is about to follow the following symbolic links: │
│                                                                     │
│   hooks  →  /home/trent/Desktop/claude/hooks (62 files, 1.2 MB)     │
│   art    →  /shared/ArcoMage/forum/ (11 files, 8.4 MB)              │
│                                                                     │
│ All files under those targets will be included in commits made      │
│ from this repo. Proceed?                                            │
│                                                                     │
│   [1] add the link path to .gitignore                               │
│   [2] continue once                                                 │
│   [3] continue always (don't ask again on this repo)                │
└─────────────────────────────────────────────────────────────────────┘

Why this exists

Git intentionally does not follow symbolic links. The maintainers have rejected this feature on the mailing list dozens of times across 20 years. Their philosophical position is that git is a content-addressable storage system and filesystem semantics belong to the shell. The technical concern is real but addressable: a malicious commit shipping a link to /etc/passwd or ~/.ssh/id_rsa is a security problem if the tool follows links blindly.

git-symlinks follows symbolic links opt-in, with the warning shown above before any data is committed or checked out. The user explicitly decides per repo whether to allow it.

How it works (no git patch, no fork)

git-symlinks extends git through four documented, frozen APIs:

Surface What we do
.gitattributes Mark paths as filter=symlinks so git pipes their content through our filter
Clean filter On git add, dereference the symlink, store target's content + metadata header
Smudge filter On git checkout, emit the stored content as a marker file
post-checkout hook Scan working tree for markers; recreate symlinks if target exists, else leave content in place

None of those surfaces have changed in over a decade. git-lfs and git-annex have ridden the same APIs across hundreds of git releases without an emergency port. git-symlinks inherits that durability for free.

The 3-piece pipeline

─── git add path/to/link ───────────────────────────────────────────────
   git reads .gitattributes → "path filter=symlinks"
   git pipes the file (raw symlink bytes) through `git-symlinks clean`
   clean filter dereferences the link, reads target content, emits:

       __GIT_SYMLINKS__\n
       target=../../shared/hooks\n
       sha256=abc123...\n
       size=12345\n
       \n
       <raw target content bytes>

   git hashes that blob and stores it. Done.

─── git checkout HEAD path/to/link ─────────────────────────────────────
   git reads the blob, pipes it through `git-symlinks smudge`.
   smudge passes the blob through unchanged → marker file at path/to/link.

   Then git fires the `post-checkout` hook.
   `git-symlinks post-checkout` walks the working tree:
     - finds files starting with __GIT_SYMLINKS__
     - for each:
        if the target path resolves on this machine → rm marker, ln -s target
        else                                       → strip header, leave content

Result: on the machine that has the target, you get a real symlink. On a machine that doesn't (CI, another developer), you get the content materialized at the link's location — repo still works.

Install

pip install git-symlinks          # not yet — placeholder
# or build from source:
git clone https://github.com/tibberous/git-symlinks
cd git-symlinks
pip install -e .

After install, in any repo:

git symlinks init                 # one-time per repo; installs filter + hook config
git symlinks add path/to/link     # equivalent to `git add` but warns + dereferences
git commit -m "track shared/hooks via deref"

Other developers cloning the repo will see the warning UX on first checkout.

Commands

Command What it does
git symlinks init Install clean/smudge filter config + post-checkout hook in the current repo. Idempotent.
git symlinks add <path> Wraps git add. Warns about each symlink-followed path before staging.
git symlinks status List currently-deref'd paths in this repo + their resolution state.
git symlinks clean Internal — called by git's clean filter. Not for direct invocation.
git symlinks smudge Internal — called by git's smudge filter.
git symlinks post-checkout Internal — called by git's post-checkout hook.
git symlinks uninstall Removes filter + hook from the current repo. Content stays in place.
git symlinks --usage Worked examples.
git symlinks --help Argparse help.

Security model

Threat Mitigation
Malicious symlink to /etc/passwd Warning UX shown before first checkout / first commit ever resolves a link. Default is refuse. User must explicitly pick "continue" or "always continue."
Symlink to outside the repo (e.g. ~/.ssh/id_rsa) Same warning + the target's path is shown in full so the user can see exactly what's about to be committed.
Symlink cycles Cycle detection — depth limit + visited-set during walk.
Stale target on another machine Falls back to materialized content (no error, no exfiltration).
User says "always continue" once then a malicious commit adds a new link A new symlink added to the repo re-triggers the warning even after "always continue" — only the previously approved links are silent.

Compatibility

Platform Status
Linux first-class
macOS first-class
Windows (real symlinks, dev mode / admin) first-class
Windows (junctions via mklink /J) supported with a warning that junction targets are not portable
Bare repos / CI works — falls back to materialized content when target unresolvable

Why not just fork git?

We considered it. The only existing attempt — Alcaro/GitBSLR — used LD_PRELOAD to intercept syscalls. Linux-only, 96 stars over years, archived 2025. The fork-and-keep-up-with-master path leads to the same place. The plugin-API path (this project) inherits git's release cadence for free and costs nothing to maintain across git versions.

Status

Pre-alpha. Spec captured, scaffolding in progress. First working pipeline target: dereference a real symlink in the canonical hooks dir scenario (see tests/hooks_scenario.md when it exists).

License

MIT.

Author

Coded by Claude Opus 4.7 · Copyright 2026 Trenton Tompkins TrentTompkins@gmail.com · trentontompkins.com · tristate.digital · github.com/tibberous · (724) 431-5207

About

Make git follow symbolic links — opt-in, consent-based, future-proof. Plugin-API tool, no git patch required.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors