Gitty is a small self-hosted Git forge for teams that want ownership, SSH-first Git hosting, pull requests, and visible AI-agent workspaces without adopting a full GitHub or GitLab clone.
The MVP is intentionally boring:
- Bun monorepo with strict TypeScript.
- Bare Git repositories stored on a filesystem volume.
- Postgres has an initialized app schema for metadata, permissions, PRs, workspace state, audit events, and auth mappings. The runnable local scaffold currently uses
METADATA_PATHJSON storage until Postgres queries are wired. - Supabase Auth / GoTrue-compatible auth is behind an
AuthAdapter, not embedded into domain logic. - OpenSSH forced-command integration keeps SSH handling battle-tested while Git authorization and command parsing stay in TypeScript.
- MCP tools expose safe forge operations only. They do not expose arbitrary shell execution.
- Source code and Git object databases live on infrastructure you control.
- The feature set is smaller and easier to reason about than a broad dev platform.
- AI agents get typed API/MCP operations for repos, pull requests, files, diffs, and workspaces.
- Agent workspaces are first-class records with leases, status, diffs, cleanup policy, and PR provenance.
bun install
bun run typecheck
bun test
cp .env.example .env
docker compose upUseful local endpoints:
- API health:
http://localhost:3000/health - Self-host settings:
http://localhost:3000/settings - Web landing page:
http://localhost:3001 - MCP tool registry:
http://localhost:3002/tools - MCP local call endpoint:
POST http://localhost:3002/call
Gitty does not store Git object databases in Postgres. Bare repositories are stored under REPO_ROOT using immutable repository IDs, for example:
/var/lib/forge/repos/repo_abc123.git
Human slugs remain metadata and URL identifiers. Immutable filesystem paths avoid traversal and rename hazards.
The current MVP path is OpenSSH forced-command:
- OpenSSH authenticates the user's public key.
- The key maps to an app user and exports
GITTY_USER_ID. - OpenSSH invokes the TypeScript entrypoint with
SSH_ORIGINAL_COMMAND. - Gitty accepts only
git-upload-packandgit-receive-pack. - Gitty resolves the repo, checks read/write permission, derives the bare repo path, and dispatches Git without shell concatenation.
Example accepted commands:
git-upload-pack 'owner/repo.git'
git-receive-pack /owner/repo.git
Rejected commands include shell fragments, unknown binaries, path traversal, and non-Git operations.
The Compose file does not currently ship a full OpenSSH daemon. This is deliberate: the TypeScript forced-command entrypoint is safe and fail-closed, but production SSH still needs host keys, authorized-key command wiring, and key-to-user mapping around it. Until that container path is completed, run OpenSSH on the host or a hardened sidecar that invokes bun run apps/sshd/src/forced-command.ts with GITTY_USER_ID, SSH_ORIGINAL_COMMAND, and METADATA_PATH set.
A workspace is the product primitive for human or AI branch isolation. It tracks:
- Repository, base ref, working branch, owner, optional agent label.
- Derived filesystem path under
WORKSPACE_ROOTusing immutable workspace IDs. - Lease owner and expiry for agent mutation coordination.
- Status and diff inspection.
- Optional associated pull request.
MVP workspaces can use native Git worktrees through createGitWorktree. The domain model also supports future isolated clones or per-workspace volumes without changing API/MCP concepts.
Run:
bun run typecheck
bun test
bun run lint
bun run db:migration:checkCurrent tests cover permission checks, safe path derivation, SSH command parsing and authorization shape, workspace leases, PR merge eligibility, and MCP tool authorization/safety.