@softspark/gitspace
v1.3.0
Published
Oh My Zsh plugin binding git identity to workspace directories: per-directory commit e-mail, gh account and SSH key, optional SSH commit signing, and an audit that finds repositories already holding the wrong identity.
Maintainers
Readme
gitspace
Git identity bound to a directory. The wrong account fails loudly instead of signing a commit quietly.
What's New in v1.3.0
- Audit exports JSON and SARIF 2.1.0 for automation and Code Scanning.
- Nested workspaces and linked worktrees are audited under their owning workspace.
- Remote URLs are redacted from audit output. Invalid history limits are rejected.
Table of Contents
- Why
- Install
- Update
- Configuration
- Usage
- Commands
- Architecture
- Key Features
- Documentation
- Known Limits
- Contributing
- Security
- License
- Changelog
Why
Working across several GitHub accounts and a private GitLab, the failure mode is not dramatic. A commit gets signed with the wrong address, nobody notices, and it surfaces weeks later in someone else's repository — or in a compliance review.
gitspace makes that hard to do by accident, on three independent layers:
- No global identity.
user.useConfigOnly=trueand no globaluser.email, so git refuses to guess an identity fromusername@hostname. - Hooks.
pre-commitandpre-pushcheck the e-mail, the SSH alias and the activeghaccount against the workspace the repository sits in. - URL rewriting.
url.<alias>.insteadOfforces every remote in a workspace onto that workspace's key.
The third layer is the one that matters. Hooks can be skipped with --no-verify
and overridden by tools that set their own core.hooksPath; the key separation
is enforced by the remote, so a push to a repository your workspace key cannot
reach fails no matter what happens locally.
Install
From git
git clone https://github.com/softspark/gitspace.git \
"${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}/plugins/gitspace"From npm
npx @softspark/gitspace installThat links the plugin into $ZSH_CUSTOM/plugins/gitspace and offers to add
gitspace to plugins=(...) in ~/.zshrc (a backup is written first; pass
--yes to skip the prompt). Nothing runs behind your back — the package ships
ignore-scripts=true and has no postinstall hook, so installation is always an
explicit command.
For a permanent install:
npm install -g @softspark/gitspace && gitspace-installThen, either way
exec zsh
gitspace installgitspace install writes the hooks to ~/.config/git/hooks, seeds
~/.config/git/workspaces.conf and sets user.useConfigOnly. It never
overwrites an existing config.
Requirements: zsh with Oh My Zsh, git 2.13+ (for includeIf), Node.js 18+
for the npm installer only. gh is optional and used
only when a workspace binds a GitHub account.
Update
npm update -g @softspark/gitspace && gitspace installRe-running gitspace install refreshes the hooks; your workspaces are untouched.
Configuration
Register a workspace and bind an e-mail to it:
gitspace add ~/Workspace/Acme \
--email [email protected] \
--gh acme-dev \
--alias github-acme,gitlab-acme \
--name "Jane Doe"Only --email is required. The path may be anywhere — workspaces are matched by
longest path prefix, so a workspace nested inside another wins over its parent.
The SSH aliases must already exist in ~/.ssh/config:
Host github-acme
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_acme
IdentitiesOnly yesgitspace add refuses to register a workspace that names an alias which does not
exist — a missing alias would silently fall back to the default key, which is the
whole failure this tool exists to prevent.
State lives in ~/.config/git/, deliberately outside the plugin directory:
deployment tooling deletes and re-clones plugin checkouts, and configuration must
survive that.
workspaces.conf
One workspace per line:
name|path|email|gh-account|ssh-alias[,ssh-alias...]|[sign]gh-account may be empty for workspaces that do not use GitHub. The sixth field
carries the signing flag and may be absent — rows written before signing existed
keep working.
Usage
Entering a directory
The chpwd hook reports the identity in force and switches the gh account when
needed. It reads the account back after switching and reports what actually
happened, so a failed switch is visible rather than assumed:
> Acme - committing as [email protected]
gh account: acme-devCloning
wclone [email protected]:acme/thing.git
wclone https://gitlab.example.com/group/sub/thing.git ~/Workspace/Acme/thingPaste a URL in any form — git@host:group/repo.git, https://host/group/repo,
or alias:group/repo. wclone derives the host, picks the workspace alias whose
HostName matches, switches the gh account, verifies access with
git ls-remote and only then clones. Nested GitLab groups work.
It refuses rather than guesses when the target lies outside every workspace, when the workspace has no alias for that host, or when the key cannot reach the repository.
Cloning through wclone rather than git clone matters: includeIf does not
apply while the repository is still being created, so a plain
git clone [email protected]:... would record a remote pointing at the default key.
Checking the setup
gitspace doctorVerifies hooks, global settings, per-workspace configs, includeIf wiring, SSH
aliases and gh login state. Exits non-zero when anything is wrong, so it can
gate a provisioning script.
It also compares the installed hooks against the plugin source. The plugin
directory is frequently a symlink to a checkout and therefore always current,
while ~/.config/git/hooks holds copies written by gitspace install — so the
hooks can be two releases behind while everything else reads as up to date. Run
gitspace install after any change under lib/.
Commands
| Command | Description |
|---|---|
| gitspace install | hooks, config template, global git settings |
| gitspace add <path> --email <addr> | register a workspace; --gh, --alias, --name, --as, --sign optional |
| gitspace list | show registered workspaces |
| gitspace doctor | verify the whole setup, exit 1 on problems |
| gitspace audit [--deep] [--limit N] [--json\|--sarif] | scan repositories for wrong remotes and identity leakage |
| gitspace remove <name> | unregister a workspace (files left on disk) |
| wclone <url> [dir] | clone with the key the target directory implies |
| gitspace-install | npm-side installer: link the plugin, patch ~/.zshrc |
Completion is provided for subcommands, workspace names, SSH aliases and gh
accounts.
Architecture
gitspace.plugin.zsh chpwd hook, wclone, the gitspace command
_gitspace zsh completion
lib/
guard.sh workspace resolution and identity check, sourced by hooks
pre-commit refuses a commit whose identity does not match
pre-push refuses a push over the wrong key or gh account
templates/
workspaces.conf seed copied on first install
bin/
gitspace-install.mjs npm installer (Node, no dependencies)
tests/run.sh suite, runs against a throwaway HOMEInstalled state, outside the plugin tree:
~/.config/git/workspaces.conf workspace table
~/.config/git/hooks/ guard.sh, pre-commit, pre-push
~/.gitconfig-<workspace> identity, hooksPath, URL rewriting
~/.gitconfig includeIf per workspaceKey Features
Path-prefix resolution. Workspaces are matched by repository path, not by directory name, so they can live anywhere and nest freely. Symlinks are resolved first — git reports physical paths, and a workspace behind a symlink would otherwise never match.
Hook chaining. The hooks run the repository's own pre-commit / pre-push
afterwards, so husky and friends keep working. Chaining goes through --git-dir
rather than --git-path, because the latter honours core.hooksPath and the
hook would exec itself forever.
No credentials touched. gitspace reads the name of the active gh account
and nothing else from that file. SSH keys are referenced by alias; no key file is
ever opened.
Truthful reporting. Every state that can fail to change is read back before being printed.
Signing
gitspace add ~/Workspace/Acme --email [email protected] --alias github-acme --signCommits and tags are then signed with that workspace's SSH key. The public key is
added to ~/.config/git/allowed_signers, so git log --show-signature verifies
locally and not only on the forge. Add the same key to the forge as a signing
key — GitHub and GitLab keep signing keys separate from authentication keys, and
uploading it once as an auth key is not enough.
Signing is off unless asked for. Turning it on by default would change behaviour for workspaces that already exist.
Auditing what is already on disk
gitspace audit # cheap: last 50 commits per repository
gitspace audit --deep # whole history
gitspace audit --json # machine-readable findings
gitspace audit --sarif # SARIF 2.1.0 for Code Scanningdoctor checks the configuration; audit checks the repositories. It reports
remotes that bypass the workspace's aliases and commits authored under another
of your workspace addresses. A colleague's address is not a finding — flagging
every third-party author turns the report into noise.
Documentation
Known Limits
git commit --no-verifybypasses the hooks. Layer 3 still applies on push.- A repository that sets its own
core.hooksPath(husky does this onnpm install) overrides the hooks for that repository. Layer 3 still applies. includeIfdoes not apply duringgit clone, which is whywcloneexists.- Workspace matching is by path prefix; a workspace nested inside another must be registered separately to win over its parent.
Contributing
See CONTRIBUTING.md.
Security
See SECURITY.md. Report vulnerabilities to [email protected] rather than in a public issue.
License
Apache-2.0. See NOTICE for attribution requirements.
Changelog
See CHANGELOG.md.
Built by SoftSpark.
