git-shed
A repository-aware link manager for local files kept outside Git.
git-shed prepares a storage directory ("shed") for each Git remote you work
with, and links it into the repository as .shed/<name>.
Notes, investigation SQL, debug scripts, local-only configuration, sample API responses, scratch code, work notes for an AI, test data — things you want next to a repository but not inside it.
~/.shed/
├── company/
└── backend/
~/src/foo/
└── .shed/
├── company -> ~/.shed/company
└── backend -> ~/.shed/backend
The real data lives outside the clone, so rm -rf on the clone keeps it, every
clone of the same remote sees the same shed, and several repositories can share
one shed. git-shed never looks inside a shed.
Install
pip install .
This puts git-shed on PATH, which is all Git needs to dispatch git shed.
Tutorial
Clone a repository and ask for a shed. Nothing matches yet, so sync offers to
create one; the defaults come from the remote:
$ git clone git@github.com:acme/api.git ~/src/api
$ cd ~/src/api
$ git shed sync
No shed matches:
github.com/acme/api
Create a new shed? [Y/n] y
Shed name [api]:
Match pattern [github.com/acme/api]:
Create shed:
name: api
match:
- github.com/acme/api
path: /home/user/.shed/api
Create this shed? (y/n) y
Created shed:
api
Linked:
.shed/api -> /home/user/.shed/api
Write whatever you want under the link. It lives outside the clone, and Git does not see it:
echo '# debugging notes' > .shed/api/notes.md
git status --short # nothing, .shed/ is in .git/info/exclude
Check out a second working tree and run sync there too. It matches the same
remote, so it gets the same shed — the notes are already there:
$ git worktree add ../api-hotfix hotfix
$ cd ../api-hotfix
$ git shed sync
Linked:
.shed/api -> /home/user/.shed/api
$ cat .shed/api/notes.md
# debugging notes
Delete either working tree and ~/.shed/api stays where it is.
Configuration
~/.config/git-shed/config.toml (or $XDG_CONFIG_HOME/git-shed/config.toml):
[[shed]]
name = "company"
match = ["github.com/acme/*"]
[[shed]]
name = "backend"
match = [
"github.com/acme/api",
"github.com/acme/worker",
]
[[shed]]
name = "personal"
match = ["github.com/me/*"]
Sheds are not exclusive: github.com/acme/api matches both company and
backend, and gets both links.
A shed name starts with an ASCII letter or digit, continues with letters,
digits, ., _ and -, and is at most 64 characters — the safe shape for a
directory name on every platform. Windows device names (CON, NUL, ...) and
a trailing . are rejected.
The data of a shed lives at ~/.shed/<name>, by convention rather than by
configuration. GIT_SHED_ROOT moves that root elsewhere, for the whole tool:
export GIT_SHED_ROOT=/data/sheds # -> /data/sheds/company
A leading ~ is expanded, since the variable is often set where a shell does
not expand it (.env files, systemd units, container environments). The value
has to denote an absolute path; a relative one is an error rather than
something resolved against the current directory.
Usage
git shed sync # create and drop links for the current repository
git shed sync --no-interactive
git shed add company 'github.com/acme/*' 'github.com/other/*'
git shed add company 'github.com/more/*' # adds to the shed that exists
git shed add # asks for whatever is missing
git shed link scratch # link a shed here even though it does not match
git shed unlink scratch # drop that link again
git shed remove company # drops the definition, keeps the data
git shed remove company 'github.com/other/*' # drops only these patterns
git shed status
git shed list # matching sheds and their patterns
git shed list --all # every defined shed and its patterns
git shed path company # /home/user/.shed/company
git shed open company # open it in the file manager
git shed list shows each shed with every pattern it is defined with, not only
the one that matched:
$ git shed list
company
github.com/acme/*
backend
github.com/acme/api
github.com/acme/worker
Shed names sit at the left margin and their patterns are indented, so
git shed list | grep -v "^ " still gives you plain names for a script.
git shed add takes a shed name followed by any number of patterns. Naming a
shed that already exists is not an error: its patterns are extended with the
ones you give, and a pattern it already has is reported instead of duplicated.
With no patterns on the command line it asks for one.
git shed remove with just a name drops the whole shed definition (never the
data). With patterns after the name it removes only those patterns and keeps
the shed; naming a pattern the shed does not have is an error.
git shed sync resolves the identities of the repository, creates
~/.shed/<name> and repo/.shed/<name> for every matching shed, drops the
links that no longer match, and adds /.shed/ to .git/info/exclude.
.gitignore is never touched.
It reports what it did:
Linked:
.shed/company -> /home/user/.shed/company
Unlinked:
.shed/old-shed
One-off links
git shed link <shed> links a shed into the current repository even though no
match rule selects it — for the occasional reference that is not worth a
pattern. The name is recorded in .shed/.sheds (one per line, # comments
allowed), which is what keeps sync from dropping the link again:
$ git shed link scratch
Linked:
.shed/scratch -> /home/user/.shed/scratch
The shed does not have to be defined in the configuration: a name whose data directory already exists under the storage root is enough, so a directory created by hand can be linked without writing a match rule for it. A name with neither a definition nor a directory is refused, which keeps a typo from creating one.
git shed unlink scratch removes the entry and the link; the data stays. A
link that comes from the configuration is not unlink's business — drop its
match rule instead.
When nothing matches the repository and the terminal is interactive, sync
offers to create a shed:
$ git shed sync
No shed matches:
github.com/acme/foo
Create a new shed? [Y/n] y
Shed name [foo]: company
Match pattern [github.com/acme/foo]: github.com/acme/*
Create shed:
name: company
match:
- github.com/acme/*
path: /home/user/.shed/company
Create this shed? (y/n) y
Remote identity
Matching uses a canonical identity rather than the raw remote URL, so all of
git@github.com:acme/foo.git
https://github.com/acme/foo.git
ssh://git@github.com/acme/foo
https://github.com/acme/foo
become
github.com/acme/foo
Every remote contributes an identity, and the sheds matching any of them are linked. In a fork checkout both the fork and the upstream repository are matched without configuring anything:
origin git@github.com:me/foo.git -> github.com/me/foo
upstream git@github.com:acme/foo.git -> github.com/acme/foo
To match on specific remotes only, name them — the setting is repeatable:
git config --local shed.remote upstream
git config --local --add shed.remote origin
A shed.remote naming a remote that does not exist is an error. A remote whose
URL cannot be interpreted is skipped with a warning, and the other remotes are
still used:
git shed: warning: ignoring remote 'helper': cannot interpret remote URL: transport::address
A repository with no usable remote is identified by the name of its working
tree directory, so ~/src/notebook gets the identity notebook and is matched
by a pattern of the same name:
[[shed]]
name = "notebook"
match = ["notebook"]
Patterns
Identities are matched segment by segment:
* one segment
** zero or more segments
github.com/acme/foo
github.com/acme/*
gitlab.com/company/**
Matching is case-insensitive, and several sheds may match the same repository — there is no priority or specificity rule to reason about.
Safety
This tool holds data that is not in Git, so it stays away from anything destructive.
-
syncandremoveonly ever delete links / junctions, never shed data. -
A real file or directory inside
.shed/is left alone, with a warning on stderr:git shed: warning: .shed/notes is not a link, left untouched
-
A changed remote does not move or delete anything;
statusshows the difference. -
Only
.git/info/excludeis modified, never.gitignore.
Platform support
Symbolic links on Linux and macOS, directory junctions on Windows. .shed/
itself is always a plain directory; only its entries are links.
Copying a project directory behaves differently on the two: cp -r keeps the
links, so the copy shares the same shed, while most Windows tools (Explorer,
xcopy, robocopy, Copy-Item) follow a junction and duplicate what is
behind it — use robocopy /XJ to leave junctions out. Running git shed sync
in the copy restores the links either way.
Exit codes
0 when the command completed — a repository matching no shed is not an error.
Non-zero for a broken configuration file, a repository or remote that cannot be
resolved, missing input for add, or a link that cannot be created.
Not in scope
The contents of a shed (layout, name collisions, backup, sync, encryption, secrets), per-shed paths, orphan cleanup, GC, migration and shell completion. It is not a replacement for git-annex or Git LFS.
Development
The test suite runs on Python 3.11 through 3.14. That matrix lives under
[tool.uv-matrix] in pyproject.toml and is driven by
uv-matrix; ruff needs no matrix:
uvx uv-matrix run --max-jobs 4 # the suite on every interpreter
uvx uv-matrix list # what the matrix expands to
uvx uv-matrix run --filter python-version=3.13
uv run --extra dev ruff check src tests
uv run --extra dev ruff format --check src tests
CI runs exactly these commands on Ubuntu. A single interpreter without uv works too:
pip install -e ".[dev]"
python -m pytest
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file git_shed-0.1.0.tar.gz.
File metadata
- Download URL: git_shed-0.1.0.tar.gz
- Upload date:
- Size: 29.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d3a30913be509bba09f99204915e8c8b60a1089543d0562efeb122622e1ddbe3
|
|
| MD5 |
f935bb5d02fca953e3d9a4cb740cbf78
|
|
| BLAKE2b-256 |
d0603953638027ea310678ea86ceb13e9666ef588e28a0a1acc96d84b5d1ba7a
|
File details
Details for the file git_shed-0.1.0-py3-none-any.whl.
File metadata
- Download URL: git_shed-0.1.0-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
85d7f67ab01a4ccccfa89b7e4d9ea0de7bfc6c8bf98d04956c0bd204b9de3647
|
|
| MD5 |
8cd9c40bd4002d9cdb1804382c8f9a80
|
|
| BLAKE2b-256 |
b1a7fed3e7966e2803c3544eaeb3972f80bce467327f671648faa1d1f499b1d7
|