Safe Git identity switching with SSH and GPG isolation
A security-focused tool for managing multiple Git identities. Prevents credential mixing between work and personal accounts through isolated SSH agents and separate GPG environments. Built in C with comprehensive security hardening and support for complex multi-organization workflows.
Separate SSH agents per account prevent key mixing
Per-account GNUPGHOME prevents signing with wrong key
Guided account creation with validation
Switch by ID, name, email, or description prefix
Preview changes before applying them
Comprehensive diagnostics for configuration issues
Per-repository or user-wide configuration
Stack protection, CFI, RELRO, safe string ops
Build Dependencies: gcc, make, openssl-devel
Runtime Dependencies: git, openssh-clients, gnupg2 (optional)
Run the interactive setup to configure an account with SSH and GPG keys:
For multiple GitHub accounts, configure SSH host aliases in ~/.ssh/config:
# Work account
Host github.com-work
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_work
IdentitiesOnly yes
# Personal account
Host github.com-personal
HostName github.com
User git
IdentityFile ~/.ssh/id_ed25519_personal
IdentitiesOnly yes Then clone using the alias:
Add to your shell configuration for persistent SSH agent environment:
# gitswitch SSH agent integration
if [ -S "$HOME/.config/gitswitch-ssh/current.sock" ]; then
export SSH_AUTH_SOCK="$HOME/.config/gitswitch-ssh/current.sock"
fi # gitswitch SSH agent integration
[[ -S "$HOME/.config/gitswitch-ssh/current.sock" ]] && \
export SSH_AUTH_SOCK="$HOME/.config/gitswitch-ssh/current.sock" # gitswitch SSH agent integration
if test -S "$HOME/.config/gitswitch-ssh/current.sock"
set -gx SSH_AUTH_SOCK "$HOME/.config/gitswitch-ssh/current.sock"
end
gitswitch stores its configuration in TOML format at ~/.config/gitswitch/accounts.toml.
The file is automatically created on first use with secure permissions (600).
[settings]
default_scope = "local"
[accounts.1]
name = "John Doe"
email = "john@work.com"
description = "Work"
[accounts.2]
name = "Johnny"
email = "johnny@gmail.com"
description = "Personal" [settings]
default_scope = "local"
[accounts.1]
name = "John Doe"
email = "john@work.com"
description = "Work - Acme Corp"
preferred_scope = "local"
ssh_enabled = true
ssh_key = "~/.ssh/id_ed25519_work"
ssh_host = "github.com-work"
gpg_enabled = true
gpg_key = "ABCD1234EFGH5678"
gpg_signing_enabled = true
[accounts.2]
name = "Johnny Personal"
email = "johnny@gmail.com"
description = "Personal projects"
preferred_scope = "global"
ssh_enabled = true
ssh_key = "~/.ssh/id_ed25519_personal"
ssh_host = "github.com-personal"
gpg_enabled = false
[accounts.3]
name = "John D"
email = "john@contractor.io"
description = "Contractor - BigCo"
preferred_scope = "local"
ssh_enabled = true
ssh_key = "~/.ssh/id_rsa_contractor"
ssh_host = "gitlab.bigco.com"
gpg_enabled = true
gpg_key = "9876FEDC5432BA10"
gpg_signing_enabled = true | Option | Type | Default | Description |
|---|---|---|---|
default_scope | string | local | Default git scope for account switching: "local" (repository) or "global" (user-wide) |
| Option | Type | Default | Description |
|---|---|---|---|
name | string | - | Git user.name for this account (required) |
email | string | - | Git user.email for this account (required) |
description | string | "" | Human-readable description for account identification |
preferred_scope | string | local | Override default scope for this account: "local" or "global" |
ssh_enabled | bool | false | Enable SSH key management for this account |
ssh_key | path | - | Path to SSH private key (supports ~ expansion) |
ssh_host | string | "" | SSH host alias for connection testing (e.g., "github.com-work") |
gpg_enabled | bool | false | Enable GPG key management for this account |
gpg_key | string | - | GPG key ID (16-character fingerprint suffix) |
gpg_signing_enabled | bool | false | Enable automatic GPG commit signing |
gitswitch respects and sets several environment variables:
| Variable | Description |
|---|---|
| SSH_AUTH_SOCK | Set to isolated SSH agent socket path on switch |
| GNUPGHOME | Set to isolated GPG directory on switch (if GPG enabled) |
| XDG_CONFIG_HOME | Respected for config file location (default: ~/.config) |
| XDG_RUNTIME_DIR | Used for GPG isolation directory (fallback: /tmp) |
| HOME | Used for ~ expansion in paths |
| NO_COLOR | Disables color output when set |
gitswitch enforces strict file permissions for security:
| File | Required Mode | Notes |
|---|---|---|
| accounts.toml | 600 | Set automatically on creation |
| SSH private keys | 600 | Validated before use |
| ~/.config/gitswitch/ | 700 | Config directory |
| ~/.config/gitswitch-ssh/ | 700 | SSH agent sockets |
The TOML parser performs extensive validation:
Run gitswitch doctor to validate your configuration.
gitswitch solves a common problem for developers who work with multiple Git identities: keeping credentials, SSH keys, and GPG signing keys properly separated. Without isolation, it's easy to accidentally commit with the wrong email, push with the wrong SSH key, or sign commits with an unintended GPG key.
The tool provides three layers of isolation:
Understanding these foundational concepts will help you configure gitswitch effectively and understand how it protects your credentials.
Select a subtopic from the sidebar to learn about Git identities, configuration scopes, and key isolation.
A Git identity consists of a name and email address stored in git configuration. These values appear in commit metadata and are used for attribution. gitswitch manages these identities as "accounts" that can include additional SSH and GPG configuration.
Git configuration exists at three levels: system, global, and local. gitswitch operates at two of these:
.git/config within a repository. Only affects that repository.~/.gitconfig. Affects all repositories for the user.Local scope is the default because it prevents accidentally using the wrong identity in a repository you haven't explicitly configured.
The primary security feature of gitswitch is key isolation. When you switch accounts:
This ensures that even if you're in the wrong directory or forget which account is active, you can't accidentally use credentials from another account.
gitswitch provides comprehensive SSH key management with isolated agents per account.
Select a subtopic to learn about agent isolation, key configuration, and host aliases.
gitswitch runs a separate ssh-agent process
for each account. The agent's socket is stored at:
~/.config/gitswitch-ssh/ssh-agent.{accountname}.sock
A symlink at ~/.config/gitswitch-ssh/current.sock
always points to the active account's socket. This symlink is what gitswitch uses to
detect the currently active account.
SSH keys must be readable by the current user with mode 600. gitswitch supports all key types that your SSH installation supports:
For services like GitHub that don't support multiple accounts on the same hostname,
use SSH host aliases. Configure these in ~/.ssh/config
and reference them in gitswitch with the ssh_host option.
When a host alias is configured, gitswitch will test SSH connectivity during account switch to verify the key is accepted:
gitswitch provides isolated GPG environments to ensure commits are signed with the correct key.
Select a subtopic to learn about GPG isolation, commit signing, and key setup.
gitswitch uses isolated GPG environments to prevent signing with the wrong key. When GPG is enabled for an account, gitswitch creates a separate GNUPGHOME:
$XDG_RUNTIME_DIR/gitswitch-gpg/{account}/
If XDG_RUNTIME_DIR is not set (common on
macOS), it falls back to /tmp/gitswitch-gpg-$UID/.
When gpg_signing_enabled is true, gitswitch
configures git to automatically sign commits:
user.signingkey to the account's GPG key IDcommit.gpgsign = trueGPG keys must exist in your system keyring before configuring them in gitswitch. To find your key ID:
Use the 16-character key ID after the algorithm (e.g., ABCD1234EFGH5678).
Common workflow patterns for using gitswitch effectively.
Select a subtopic to see configuration examples for different scenarios.
The most common use case is separating work and personal GitHub accounts:
~/.ssh/configgit@github.com-work:org/repo.gitgit@github.com-personal:user/repo.gitFor developers working across multiple organizations (consulting, open source maintainers):
Contractors often need to access multiple client Git servers with different credentials:
[accounts.1]
name = "John Contractor"
email = "john@contractor.io"
description = "Personal - Default"
preferred_scope = "global"
[accounts.2]
name = "John Doe"
email = "john.doe@client-a.com"
description = "Client A - Finance Corp"
ssh_enabled = true
ssh_key = "~/.ssh/id_ed25519_client_a"
ssh_host = "gitlab.client-a.com"
[accounts.3]
name = "J. Doe"
email = "jdoe@client-b.io"
description = "Client B - Tech Startup"
ssh_enabled = true
ssh_key = "~/.ssh/id_rsa_client_b"
ssh_host = "github.client-b.io"
gpg_enabled = true
gpg_key = "FEDCBA9876543210"
gpg_signing_enabled = true gitswitch is designed with security as a primary concern:
-fstack-protector-strong)gitswitch is built in C (GNU C11) with a modular architecture organized into five development phases. The codebase consists of approximately 250KB across 10 core source files, each handling a specific concern.
| Module | Size | Responsibility |
|---|---|---|
| main.c | 14KB | CLI parsing, command dispatch |
| accounts.c | 39KB | Account operations, switching logic |
| config.c | 28KB | TOML configuration management |
| toml_parser.c | 33KB | Custom TOML parser with validation |
| git_ops.c | 21KB | Git configuration integration |
| ssh_manager.c | 29KB | SSH agent isolation |
| gpg_manager.c | 24KB | GPG environment isolation |
| display.c | 23KB | Terminal UI and formatting |
| error.c | 14KB | Error handling and logging |
| utils.c | 29KB | Helper functions, safe strings |
Account switching follows a strict lifecycle to ensure clean transitions:
accounts_switch(ctx, account_identifier)
├── 1. Lookup account by ID/name/email/description
├── 2. Validate account configuration
├── 3. Cleanup previous session
│ ├── Stop owned SSH agents
│ ├── Restore original GNUPGHOME
│ └── Clear session state
├── 4. Configure git identity
│ ├── Set user.name
│ ├── Set user.email
│ └── Verify configuration applied
├── 5. Setup SSH isolation (if enabled)
│ ├── Create isolated agent
│ ├── Load account key
│ ├── Update current.sock symlink
│ └── Test connectivity (if host configured)
├── 6. Setup GPG isolation (if enabled)
│ ├── Create isolated GNUPGHOME
│ ├── Import key from system keyring
│ ├── Configure signing key
│ └── Test signing capability
└── 7. Update session state Cleanup happens at the start of a new switch, not at the end. This allows SSH agents to persist across commands while ensuring no resource leaks when switching.
gitswitch operates in SSH_AGENT_ISOLATED mode,
creating dedicated agents per account. The architecture ensures complete separation:
typedef struct {
ssh_agent_mode_t mode; // ISOLATED, SYSTEM, or NONE
char agent_socket_path[4096]; // Path to agent socket
pid_t agent_pid; // Agent process ID
bool agent_owned; // Did we start this agent?
} ssh_config_t; SSH agent sockets follow a predictable naming scheme:
~/.config/gitswitch-ssh/
├── ssh-agent.john-work.sock # Account 1's agent
├── ssh-agent.johnny-personal.sock # Account 2's agent
└── current.sock -> ssh-agent.john-work.sock # Symlink to active
The current.sock symlink serves two purposes:
Keys are loaded into the agent using ssh-add
with the following process:
ssh-add {key_path} with new agent's socketssh -T git@{host}GPG uses the GNUPGHOME environment variable to locate its keyring and configuration. gitswitch creates isolated directories for each account:
$XDG_RUNTIME_DIR/gitswitch-gpg/
├── john-work/
│ ├── pubring.kbx
│ ├── trustdb.gpg
│ └── private-keys-v1.d/
└── johnny-personal/
├── pubring.kbx
├── trustdb.gpg
└── private-keys-v1.d/ The runtime directory is chosen because:
Keys are imported from the system keyring to the isolated environment on each switch:
# Export from system keyring
gpg --export-secret-keys {key_id} | \
GNUPGHOME={isolated_dir} gpg --import
# Trust the imported key
GNUPGHOME={isolated_dir} gpg --edit-key {key_id} trust quit When GPG signing is enabled, gitswitch configures git and verifies the setup:
git config user.signingkey {key_id}git config commit.gpgsign trueecho test | gpg --sign
The core data structures are defined in gitswitch.h:
typedef struct {
uint32_t id; // Unique account identifier
char name[256]; // Git user.name
char email[320]; // Git user.email (RFC 5321 max)
char description[512]; // Human-readable description
git_scope_t preferred_scope; // LOCAL or GLOBAL
// SSH configuration
bool ssh_enabled;
char ssh_key_path[4096]; // PATH_MAX
char ssh_host_alias[256]; // Optional host for testing
// GPG configuration
bool gpg_enabled;
bool gpg_signing_enabled;
char gpg_key_id[64]; // 16-char hex + buffer
} account_t; typedef struct {
config_t config; // Global settings
account_t accounts[64]; // Account storage (max 64)
size_t account_count; // Number of configured accounts
account_t *current_account; // Pointer to active account
// Session state
ssh_config_t ssh; // SSH agent state
gpg_config_t gpg; // GPG isolation state
// Runtime flags
bool verbose;
bool dry_run;
bool color_output;
} gitswitch_ctx_t; typedef struct {
uint32_t account_id; // Currently active account
pid_t ssh_agent_pid; // PID of owned SSH agent
char ssh_socket_path[4096]; // Path to agent socket
char original_gnupghome[4096]; // GNUPGHOME before switch
char isolated_gnupghome[4096]; // Current isolated GNUPGHOME
} active_session_t; The TOML parser includes extensive security checks:
../)// All string operations use safe wrappers
bool safe_strncpy(char *dest, size_t dest_size,
const char *src, size_t max_len);
bool safe_strncat(char *dest, size_t dest_size,
const char *src, size_t max_len);
bool safe_snprintf(char *dest, size_t dest_size,
const char *fmt, ...);
// Memory is zeroed for sensitive data
void secure_zero(void *ptr, size_t len); The Makefile includes comprehensive security flags:
SECURITY_FLAGS = -fstack-protector-strong \
-fcf-protection \
-D_FORTIFY_SOURCE=2 \
-Wformat -Wformat-security \
-fPIE
LDFLAGS = -Wl,-z,relro,-z,now \
-Wl,-z,noexecstack \
-pie | Option | Type | Default | Description |
|---|---|---|---|
add | - | - | Add a new account interactively with guided prompts |
list, ls | - | - | Display all configured accounts with current status |
remove, rm, delete | - | - | Remove specified account by ID, name, or email |
status | - | - | Show currently active account and configuration |
doctor, health | - | - | Run comprehensive health check on configuration and keys |
config | - | - | Display configuration file location and contents |
<account> | - | - | Switch to account by ID, name, email, or description prefix |
| Option | Type | Default | Description |
|---|---|---|---|
--help, -h | flag | - | Show help message and usage information |
--version, -v | flag | - | Display version number |
--global, -g | flag | - | Use global git scope (user-wide configuration) |
--local, -l | flag | - | Use local git scope (repository-specific) |
--dry-run, -n | flag | - | Show changes without executing them |
--verbose, -V | flag | - | Enable verbose output with detailed information |
--debug, -d | flag | - | Enable debug logging for troubleshooting |
--color, -c | flag | - | Force color output even when not a TTY |
--no-color, -C | flag | - | Disable color output |
| Option | Type | Default | Description |
|---|---|---|---|
ERR_ACCOUNT_NOT_FOUND | error | 10 | Specified account does not exist in configuration |
ERR_SSH_KEY_NOT_FOUND | error | 20 | SSH key file does not exist or is not readable |
ERR_SSH_AGENT_FAILED | error | 21 | Failed to start or connect to SSH agent |
ERR_SSH_KEY_LOAD_FAILED | error | 22 | Failed to load SSH key into agent |
ERR_GPG_KEY_NOT_FOUND | error | 30 | GPG key ID not found in keyring |
ERR_GPG_SIGNING_FAILED | error | 31 | GPG signing test failed |
ERR_CONFIG_PARSE | error | 40 | Failed to parse configuration file |
ERR_CONFIG_WRITE | error | 41 | Failed to write configuration file |
ERR_GIT_CONFIG_FAILED | error | 50 | Failed to set git configuration |
ERR_PERMISSION_DENIED | error | 60 | File permission error (keys must be 600) |
The SSH_AUTH_SOCK environment variable isn't being picked up by your shell.
Solution:
source ~/.bashrcexport SSH_AUTH_SOCK=~/.config/gitswitch-ssh/current.sockSSH keys must have mode 600 (owner read/write only).
Solution:
The GPG key hasn't been imported into the system keyring.
Solution:
gpg --list-secret-keysgpg --import your-key.ascgitswitch doctor to verifyGit is using global config instead of local, or you forgot to switch accounts.
Solution:
gitswitch statusgit config --local user.emailgitswitch workgit commit --amend --reset-authorGitHub only allows one SSH key per account, and uses key to determine identity.
Solution:
~/.ssh/configgit@github.com-work:org/repo.gitgit remote set-url origin git@github.com-work:org/repo.gitThe TOML configuration file has a syntax error.
Solution:
gitswitch configmv ~/.config/gitswitch/accounts.toml accounts.toml.bak && gitswitch addSSH agents are per-session by design. Shell integration is needed for persistence.
Solution:
The search term doesn't match any account ID, name, email, or description.
Solution:
gitswitch listgitswitch 1gitswitch john@work.comgitswitch --help for usage informationgitswitch doctor for configuration validationgitswitch --debug --verbose output