Skip to main content

Tiny Horse

Tiny Horse is an open-source, Git-native control layer for projects that should remain usable on local or user-owned infrastructure.

It does not replace Git. It wraps standard Git operations with a small, stable interface that humans and agents can inspect without depending on GitHub as the source of truth.

What's new in 0.2.5

Restored linked worktrees now remain usable after pull moves its temporary checkout to the final destination. Tiny Horse repairs both directions of the Git administrative links and checks that each worktree points into the recovered repository. This also applies to clone and claim, which use the same recovery workflow.

Upgrade with:

python -m pip install --upgrade 'tinyhorse[gdrive]'

The regression suite covers recovery through the public pull operation, including worktree contents, Git commands, and administrative links after relocation. This release does not change the canonical bundle format.

The storage loop

A Tiny Horse project has one canonical Git bundle on user-controlled storage. The repository contains a .tinyhorse.toml locator, not a mutable "current release" pointer. HEAD, refs, tags, timestamps, and history come from Git itself.

# Bind an existing repo to a local canonical object.
tinyhorse init . --storage file:/srv/git/my-project.bundle

# Safe by default: only fast-forwards canonical Git and preserves refs.
tinyhorse push
tinyhorse doctor

# Recover on another machine.
tinyhorse pull file:/srv/git/my-project.bundle ./my-project

# Propose without moving canonical main; accept fast-forwards then parks.
tinyhorse propose --id my-change
tinyhorse accept my-change

Google Drive login

Install the Drive extra once:

python -m pip install 'tinyhorse[gdrive]'

Tiny Horse uses Google's installed-app OAuth flow. It opens the browser, obtains a refresh token, and stores the credential outside the repository. You do not copy access tokens into shell variables for normal interactive use.

For development builds, provide a Google OAuth Desktop app client JSON once:

tinyhorse auth login --client-secrets ~/Downloads/client_secret.json

You can instead set TINYHORSE_GOOGLE_CLIENT_SECRETS or place that JSON at ~/.config/tinyhorse/google-client.json. On first login Tiny Horse remembers the client configuration in its user config directory, so later logins do not need the path again. The resolver also supports a packaged google-client.json, allowing a first-party Tiny Horse OAuth client to remove this development-only step without changing project configuration.

tinyhorse auth status
# To force a refresh while checking:
tinyhorse auth status --refresh

tinyhorse auth logout

Saved refreshable credentials default to ~/.config/tinyhorse/google-drive-credentials.json (or the platform/config override). The file is written with owner-only permissions where the OS supports them. TINYHORSE_GOOGLE_DRIVE_TOKEN is still accepted for CI and other intentionally non-interactive environments.

Then Drive setup no longer requires a folder ID or a gdrive:// locator:

tinyhorse init . --drive
tinyhorse push
tinyhorse doctor

init --drive looks for one top-level Drive folder with the project name, creates it if needed, adopts an existing single .bundle inside it when present, and otherwise chooses a deterministic bundle filename. The resolved locator is stored in .tinyhorse.toml; it is implementation detail rather than user input.

Explicit gdrive://FOLDER_ID/FILENAME locators remain supported for automation and unusual layouts. Fresh-machine recovery no longer requires a locator. After authentication:

tinyhorse projects
tinyhorse clone TULKAS

Tiny verbs

Autonomous git control uses short names. They wrap the same storage loop. tinyhorse --help repeats this; every subcommand has a description.

Tiny Same as / meaning
tinyhorse park push — publish the canonical bundle, including parked WIP
tinyhorse claim NAME clone — restore a Drive project by name
tinyhorse doctor compare local vs canonical; dirty is ok if that dirt is parked
tinyhorse freeze-writer refuse park/push from this checkout
tinyhorse freeze-writer --clear unfreeze (or delete .git/tinyhorse-freeze)
tinyhorse from-zip ZIP DEST GitHub zipball → one new commit (no history)
tinyhorse ingest-github URL migrate a git URL → full history
tinyhorse tag v1.2.0 annotated git tag on your project, then park

WIP (uncommitted files, extra worktrees, refs/backup/) is snapshotted into refs/tinyhorse/wip/<name> inside the same bundle. Park drops wip refs for worktrees that are gone or clean. An in-progress rebase/merge is refused until packed.

Your versioning is git tags on the project, not tinyhorse --version. tinyhorse tag v1.0 creates git tag -a v1.0 and parks so the tag lives in the canonical bundle.

MCP: tinyhorse-mcp --help then stdio JSON-RPC (park, claim, doctor, from_zip, status, tag).

projects discovers top-level Drive project folders that are marked by Tiny Horse or contain Git bundles, surfaces empty/ambiguous folders instead of guessing, and does not print provider folder/file IDs. clone PROJECT requires one exact project name, restores the canonical bundle, writes the machine-local binding when needed, and restores parked WIP (the tree may be dirty; that is success). Explicit pull LOCATOR remains available for automation and unusual layouts.

The Drive adapter searches for exactly one canonical file with that name in that folder. push creates it once and then updates the same Drive object in place. Multiple same-named candidates are treated as an error instead of guessed around. Before replacing an existing object, Tiny Horse downloads and verifies canonical Git, compares its durable refs with the local bundle, and writes only with the snapshot token from that inspect. File storage serializes readers and writers with an exclusive lock, hashes the bytes it copies, and fsyncs unique temporary files before replace. Drive content replacement uses Drive API v2 If-Match so a changed object returns HTTP 412 instead of a silent overwrite. It then reads the object back and requires the SHA-256 of the downloaded bytes to match the upload. See docs/STORAGE_SAFETY.md.

One-command GitHub migration

Once Drive authentication is configured, a GitHub repository can be moved into Tiny Horse canonical storage with one command:

tinyhorse migrate https://github.com/owner/repository.git

Tiny Horse derives the project and local checkout names from the remote, takes a read-only mirror snapshot, materializes every source branch plus tags and Git notes in a normal working checkout, auto-provisions the Drive project folder, safely publishes the canonical bundle, downloads it again, and verifies that its durable Git refs exactly match the source snapshot. The GitHub repository is never pushed to, rewritten, deleted, archived, or otherwise modified by migration.

The original remote remains configured locally as origin, so it can still be used as an optional legacy/read source. Canonical authority is the Tiny Horse storage binding. For an exact-history import, the generated .tinyhorse.toml is kept machine-local via .git/info/exclude; migration does not add a setup commit or otherwise alter the imported Git history.

Useful overrides are available without changing the safe default:

# Pick a different checkout directory or display name.
tinyhorse migrate https://github.com/owner/repository.git ./repo --project "My Project"

# Use any supported user-controlled storage instead of Drive.
tinyhorse migrate https://github.com/owner/repository.git --storage file:/srv/git/repository.bundle

If the source already tracks a .tinyhorse.toml, migration refuses to replace that existing binding implicitly.

Safe push

Normal tinyhorse push is deliberately conservative. Canonical branches may only move forward, canonical tags and other durable refs may not be silently rewritten or deleted, and canonical HEAD may not be replaced by a stale or divergent local history. If another writer changes storage after Tiny Horse's preflight read, the push aborts rather than knowingly overwriting that newer state.

For an intentional history rewrite or ref deletion, use tinyhorse push --force. Force bypasses the Git ancestry/ref policy, not the storage-version check or readback verification. Remote-tracking refs (refs/remotes/*) are treated as caches and are no longer packaged as canonical project state.

CLI

tinyhorse --help
tinyhorse status [PATH]
tinyhorse init [PATH] (--drive | --storage LOCATOR) [--project NAME]
tinyhorse park [PATH] [--force]          # also: push
tinyhorse pull LOCATOR DESTINATION
tinyhorse doctor [PATH]
tinyhorse projects
tinyhorse claim PROJECT [DESTINATION]    # also: clone
tinyhorse tag NAME [PATH] [-m MSG] [--no-park]
tinyhorse freeze-writer [PATH] [--clear]
tinyhorse from-zip ZIP DEST [--project NAME] [--storage LOCATOR]
tinyhorse ingest-github URL [DEST] [--drive | --storage LOCATOR] [--project NAME]
tinyhorse bundle PATH OUTPUT
tinyhorse verify-bundle BUNDLE
tinyhorse migrate REMOTE [DESTINATION] [--drive | --storage LOCATOR] [--project NAME]
tinyhorse auth login [--client-secrets PATH] [--no-browser]
tinyhorse auth status [--refresh]
tinyhorse auth logout
tinyhorse-mcp --help

status, migrate, push, pull, doctor, and the auth commands produce machine-readable JSON where appropriate. doctor compares local HEAD to the canonical bundle and reports in_sync, local_ahead, local_behind, or diverged_or_unrelated. A refused safe push exits without replacing canonical storage and explains which HEAD, branch, tag, or ref would have been lost.

Why TULKAS matters

TULKAS is the first proving case. It exposed a practical failure mode: when live Git, old GitHub snapshots, and point-in-time backups sit together, a fresh agent can guess wrong about what is current. Tiny Horse's rule is deliberately mechanical: one canonical Git object, archives elsewhere, and no hand-maintained freshness metadata.

Package release gate

The repository includes a repeatable package release check and artifact builder:

python tools/release.py check
python tools/release.py build --allow-dev   # development candidate

Tiny Horse is licensed under Apache-2.0. A public release requires a final X.Y.Z version, a matching vX.Y.Z Git tag, and package-index credentials. The build is made from committed HEAD, verifies wheel/sdist metadata, smoke-installs the wheel in a clean virtual environment, and writes SHA-256 hashes. See docs/RELEASING.md.

Development

Requires Python 3.10 or later. On 3.10, tomli is installed automatically so config parsing does not depend on tomllib from 3.11.

python -m pip install -e .
python -m unittest discover -s tests -v

The source distribution includes tests/ and tools/release.py, so the same discovery command works from a published sdist unpack.

Release files for tinyhorse 0.2.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tinyhorse 0.2.5
File Size Uploaded
tinyhorse-0.2.5.tar.gz 68.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tinyhorse 0.2.5
File Interpreter ABI Platform
tinyhorse-0.2.5-py3-none-any.whl Python 3 none any Details

Total release size: 110.7 kB

Release files / tinyhorse-0.2.5.tar.gz

Download URL tinyhorse-0.2.5.tar.gz
Size 68.1 kB
Tags Source
SHA-256 checksum
How to use checksums
c1dc60db4b9b932d93d532d7d41844a0fefdfbd775298ab34e31d831e785ac4d
BLAKE2b-256 checksum
How to use checksums
e9d287f59c54476a8ee511784308f373c104fc6bc1c0573226a9313d278ad6e7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release files / tinyhorse-0.2.5-py3-none-any.whl

Download URL tinyhorse-0.2.5-py3-none-any.whl
Size 42.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c5d060d7415d08498ca0f4f1bbd717f2489217938b2aee9b96a189eff0173bfd
BLAKE2b-256 checksum
How to use checksums
aa2ba44cf55c7ed8382e911c80c93528f002368b9d05619913797e70565d8560
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.6

Release history Release notifications | RSS feed

This release

0.2.5 This release

2 release files

0.2.4

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page