Repositories¶
See also: Skills and Hooks (what a repository provides), Projects (where a repository's skills get installed), and MCP Servers (a repository can also declare MCP presets).
Overview¶
A repository is any Git remote that contains one or more skills (and, optionally, MCP server presets - see MCP Servers). SkillKeeper clones it locally, resolves the skills it contains, and tracks it for updates.
Repository record fields: id, name, url, kind (github, bitbucket,
or generic - detected from the URL), transport (ssh or https -
likewise detected), lfs (whether Git LFS is required), localPath,
lastFetched, and an optional branch - a user-chosen branch to track
instead of the clone's default.
Adding and managing repositories¶
CLI:
skillkeeper repo add <url> [<local-path>] [--name <name>] [--lfs | --no-lfs]
skillkeeper repo remove <id>
skillkeeper repo list
skillkeeper repo update [id] [--all]
skillkeeper repo lint [<id> | --all | --path <dir>] [--json]
repo add clones immediately. The local path is optional: omit it and the
clone lands in a per-repository directory under the app's own repositories folder
(the same location the desktop app uses). Git LFS is enabled by default when the
git-lfs client is installed; --no-lfs opts out. The
desktop app splits this in two: adding a repository first records it (so its
card can appear right away) at an app-managed path under its own
repositories directory, then clones it as a second step; a repository whose
clone has not finished, or failed, still has a record and can be retried.
Either front end rejects an add whose URL (or, for the CLI, local path) is
already tracked.
repo update (CLI) or "sync" (desktop) refreshes a repository from its
remote; see "Update operations" below for how the two differ.
To check what is statically wrong with a repository's skills -- missing
dependencies, cycles, skills that do not resolve -- run
repo lint.
Branch tracking¶
A repository can track a specific branch instead of the clone's default. Setting or changing the tracked branch force-checks-out that branch in the clone (discarding any local edits); from then on, every sync first force-switches to the tracked branch, then force-pulls, so the clone always ends up on that branch even if something else changed it out from under SkillKeeper. Leaving the branch unset just means "whatever the remote's default branch is."
SSH transport and private repositories¶
SkillKeeper supports SSH transport for private GitHub and Bitbucket
repositories. Key material and passphrases are handled by the user's
ssh-agent, unless a key is chosen in Settings - see
Using a dedicated SSH key for what the app
reads and holds in that case. In the desktop app, a
repository's clone and sync run inside the app's own embedded terminal
session rather than silently in the background - so if the ssh-agent needs a
passphrase, the prompt surfaces there, and the app can open the terminal
automatically when input is needed. A "run ssh-add" action is available from
the same terminal to load a key into the agent. Update checks are partly
interactive: the background "check for updates" runs its fetch through
the same terminal-backed Git as a pull (visible, ssh-capable, and it can
prompt for an SSH passphrase); only the two rev-parse comparisons that
follow the fetch run through the silent Git invocation.
Example SSH URL:
skillkeeper repo add git@github.com:example/private-skills.git ./private-skills
Make sure your ssh-agent has the relevant key loaded before running this command; the CLI has no embedded terminal, so an interactive passphrase prompt there depends entirely on your own shell's ssh-agent integration.
Setting up an ssh-agent¶
An ssh-agent holds your unlocked private key in memory, so Git can authenticate without asking for the passphrase again. SkillKeeper works without one -- the desktop app opens its terminal and you type the passphrase each time -- but with an agent, clone and sync run without interruption.
With no key chosen in Settings, SkillKeeper reads neither your key nor your passphrase: it reuses whatever agent your system already provides. With one chosen, it reads that key file to verify the passphrase you give it - see Using a dedicated SSH key.
macOS¶
An agent runs by default. Add your key once and store the passphrase in the system keychain:
ssh-add --apple-use-keychain ~/.ssh/id_ed25519
To have it loaded automatically on every login, add this to ~/.ssh/config:
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
Linux¶
Most desktop environments start an agent at login (GNOME Keyring, KDE Wallet). Check with:
echo "$SSH_AUTH_SOCK"
If it prints nothing, start one for the session:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
To make that permanent, enable the user service shipped with OpenSSH:
systemctl --user enable --now ssh-agent.service
and add AddKeysToAgent yes under Host * in ~/.ssh/config.
Windows¶
Windows ships an OpenSSH agent as a service. Enable it once, from an elevated PowerShell:
Set-Service ssh-agent -StartupType Automatic
Start-Service ssh-agent
ssh-add $env:USERPROFILE\.ssh\id_ed25519
Important: Git for Windows uses its own bundled ssh by default, and that
one does not talk to the Windows agent service. Point Git at the system client
so the key you just added is actually used:
git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"
Without this, the agent holds your key but Git keeps asking for the passphrase.
Passphrase prompts and pinentry¶
When no agent holds the key, ssh has to ask for the passphrase. It reads it
from the terminal it is attached to -- which is why the desktop app runs Git
inside its embedded terminal and brings that terminal to the front when input
is needed.
A pinentry program replaces that in-terminal prompt with a small dialog. It is
only involved if you route SSH through gpg-agent instead of ssh-agent,
which is worth doing when your key lives on a smartcard or a YubiKey. Enable
SSH support in ~/.gnupg/gpg-agent.conf:
enable-ssh-support
pinentry-program /usr/bin/pinentry-gtk-2
Use pinentry-mac on macOS (brew install pinentry-mac) and
pinentry-gtk-2, pinentry-qt or pinentry-curses on Linux, then point
SSH_AUTH_SOCK at the gpg-agent socket:
export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)"
If you are not using GnuPG, you do not need pinentry at all -- a plain ssh-agent is enough.
Using a dedicated SSH key¶
The desktop app can use one specific private key for SSH remotes instead of
whatever ssh would pick. Choose it in Settings, under Repositories; the path
is stored as repositories.sshKeyPath and the CLI honours it too.
With a key chosen, Git runs as ssh -i <key>. The key is offered, not enforced:
if a host does not accept it, ssh falls back to your own ~/.ssh/config
identities and your agent, so a repository on a host this key has no access to
keeps working. With no key chosen, nothing changes.
If the key has a passphrase, the app asks for it in a separate window, which blocks the main window while it is up, and verifies it immediately. It is held in memory for that run of the app only: never written to the config or to disk, and asked again after a restart, the first time an operation actually needs it. A scheduled update check never blocks on it -- it raises the same window and resumes once the key is unlocked.
A few things worth knowing:
- The chosen key is one identity among the ones
sshmay try, so on a host that accepts several of your keys it is not guaranteed to be the one used. If you need a specific identity for a specific host, say so in~/.ssh/config. - An unknown host key must be confirmed once in the app's terminal. While the
app answers passphrase prompts itself,
sshcannot ask anything else there. - The CLI reads the same setting but keeps no passphrase, so
sshasks in the terminal you ran it from. Permission denied (publickey)looks the same whether the passphrase never reachedsshor the host does not accept the key. Start the app withSKILLKEEPER_SSH_VERBOSE=1to run every git operation asssh -v, and the terminal shows which it is.
PuTTY keys (.ppk)¶
The desktop app reads PuTTY-format keys directly. Choose the .ppk in Settings
as you would any other key; on the first SSH operation the app asks for its
passphrase, decrypts the key in memory, and loads it into your ssh-agent for
twelve hours. The key file is never rewritten and no converted copy is stored:
the decrypted key goes straight from memory into the agent, and the app removes
it again when it quits.
This needs a running ssh-agent, because that is where the key lives once it is loaded. On Windows the OpenSSH Authentication Agent service ships disabled -- see "Setting up an ssh-agent" above for how to start it.
Settings also offers Convert to an OpenSSH key -- the warning icon beside
the key path -- for any PuTTY key that is not currently loaded in the agent. That is the way out when no agent is
available, and it is offered the rest of the time too, since an agent can be
present and still refuse the key. It writes a new key file where you choose,
encrypted with the same passphrase, and switches repositories.sshKeyPath to
it. If ssh then reports that the new
file's permissions are too open on Windows, restrict it with:
icacls "%USERPROFILE%\.ssh\id_converted" /inheritance:r /grant:r "%USERNAME%:R"
The CLI does not read PuTTY keys. With one configured it prints the puttygen
command that converts it and otherwise leaves your agent and ~/.ssh/config to
work as usual.
Git LFS¶
If a repository declares Git LFS usage, SkillKeeper runs git lfs steps
during clone and sync. If the git-lfs client is absent, SkillKeeper reports
a clear, actionable error rather than silently producing an incomplete
checkout.
How Git is invoked¶
SkillKeeper invokes the system git binary as a subprocess with argument
arrays only - no shell string interpolation. This design:
- Reuses the user's existing SSH config, ssh-agent, and Git LFS setup.
- Keeps credential handling out of the application.
- Prevents shell injection.
- Reduces the dependency and attack surface compared to a Node Git library.
Supported operations: clone, fetch, pull (fast-forward only),
forcePull (fetch, then reset --hard @{upstream} plus clean -fd),
checkout <branch> (force-switch, discarding local edits), lfsPull,
setRemoteUrl, revParse, currentBranch, and listBranches.
Update detection¶
SkillKeeper uses two levels of update detection, matching repositories and skills separately:
Repository level (repoHasUpdate): fetch, then compare the local HEAD
to the tracked upstream ref (@{upstream}). If they differ, the repository
"can be updated." Read-only: a fetch alone never modifies the working tree.
Skill level (skillHasUpdate): compare the content hash of a resolved
skill's files in the working tree against the content hash recorded in its
InstallManifest (falling back to recomputing it from the manifest's file
hashes when no hash was recorded). A skill "can be updated" when the source
content differs from what is installed - a changed, added, or removed file.
This distinction matters: a repository may have new commits without any of the skills you have installed changing, or a skill's files may change without the tracked ref moving (for example after a force-push that lands at the same ref).
Update operations¶
The CLI and the desktop app expose different-shaped update operations over the same core primitives:
- CLI
repo updateruns a plain fast-forward-onlygit pullon one repository or every tracked repository (--all); it never touches individual skills or their installs. - CLI
skill update <id>re-resolves one installed skill from its source repository and reinstalls it (optionally limited to--agent), independent of any repository update. - Desktop "sync" on a repository card is the stronger operation: it
force-switches to the tracked branch (if set), then force-pulls - fetch
plus a hard reset to the upstream ref plus a clean of untracked files - so
an app-managed clone always ends up byte-identical to the remote and never
hits a merge conflict. It never modifies any already-installed skill by
itself; it only makes new/changed skill content visible for the next
install or update action. A repository's card also carries a background
"check for updates" (
repoHasUpdate) that fetches and compares refs without pulling, so the update indicator can appear well before the user chooses to sync. - Desktop project-mode skill updates are chosen per project, driven by which installed skills are stale relative to their source repository; the desktop app applies them the same way it applies fresh installs (see Projects).
Skill availability¶
listAvailableSkills resolves the skills in every cloned repository's
working tree (the same resolution schemes described in
Skills and Hooks) for the
Skills page's browsing tree. A repository whose clone is missing, or whose
working tree fails to resolve (for example a malformed
skillkeeper.repo.yaml), is skipped for that listing; the others still
appear.
Removing a repository¶
Removing a repository only drops its tracked record and best-effort deletes its local clone directory. Skills already installed from it are not uninstalled - they keep working, and the next reconciliation pass (see Skills and Hooks) remembers their last known repository id rather than treating them as freshly unmanaged. They simply stop being able to show an "update available" badge, since there is no longer a tracked repository to compare their content hash against.
Update modes¶
By default, SkillKeeper checks for updates on demand only. The updates
section of config.yaml controls this behavior. See
Configuration for details.