Tiny Horse
Tiny Horse is an experimental, 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.3.0
Opt-in descriptor authorities support large full bundles while keeping the canonical update to one small conditional pointer replacement. Container discovery and verified duplicate resolution from 0.2.9 remain supported. Raw strict Drive replacement retains its 16 MiB cap. Deterministic preflight refusals are classified separately from unknown remote outcomes; all reviews remain consumed.
See descriptor workflows, compatibility and read-only recovery. Migration requires coordinated writer cutover; there is no automatic conversion.
What's new in 0.2.9
- One-level Google Drive container discovery:
projectsandclaim/clonescan top-level Drive folders plus one level inside configured container folders (default["PYPI DEV TINYHORSE PROJECTS"]), configurable viacontainersin.tinyhorse.tomlor userconfig.toml,TINYHORSE_CONTAINERS, CLI--container, and MCPcontainerarguments. - Verified
.tinyhorse.tomlduplicate disambiguation: When a project name appears in both top-level and container Drive folders, Tiny Horse verifies.tinyhorse.tomlproject and normalizedgdrive://locator bindings (honoring the current repository's local.tinyhorse.tomlwhenrepois omitted, inspecting all.tinyhorse.tomlfiles in candidate Drive folders, rejecting multi-config or conflicting bindings, and surfacing duplicate warnings in CLI stderr and MCP JSON responses). - Container persistence:
init --drive --container,ingest-github --container, andclaim/clonepersist discovered or specified container folder names into project and user configuration without mutating tracked.tinyhorse.tomlfiles.
What's new in 0.2.8
- Managed Google Drive OAuth refresh before metadata reads, resumable session creation, and each upload chunk/probe.
- Durable ref preservation (
refs/notes/*,refs/backup/*,refs/codex/*,refs/strict/*) acrossproposeand working-tree recovery. - Linked Git worktree inspection in
doctor, batched Drive project discovery, hardenedfrom-zipextraction, PEP 561py.typedtyping marker, and full CLI/MCP tool parity.
What's new in 0.2.7
Opt-in strict-prepare / strict-publish retains the reviewed storage snapshot
and sends one conditional content replacement, with no refreshed validator,
publication retry or automatic rollback. Existing park/push behavior is unchanged.
Concurrent Drive writers must use strict mode. Live synthetic tests found
that Drive v3 media/resumable requests accepted stale v2 ETags, including a
competing update after session initiation. Do not rely on ordinary resumable
park for strict concurrency protection. Strict mode uses a single Drive v2
media PUT carrying If-Match on the content request; live tests rejected stale
conditions with HTTP 412. It supports existing objects up to 16 MiB and refuses
larger payloads without falling back to resumable uploads.
tinyhorse strict-prepare . --review-dir ../review-unique
# Review the exact refs, hashes, storage revision and returned review_sha256.
tinyhorse strict-publish ../review-unique --review-sha256 <exact-reviewed-sha256>
Review directories contain private bundles and should remain outside the checkout.
Success, conflict and uncertain outcome consume the attempt. Inspect an uncertain
outcome before making a separately authorized new review. Strict mode never
creates the initial canonical object or changes Git refs. See
docs/STRICT_PUBLICATION.md.
What's new in 0.2.6
Google Drive bundle uploads use resumable 16 MiB chunks. A push that times out
continues from the byte offset Drive reports, including the next tinyhorse push.
The resumable route is retained for compatibility, but its prior concurrency
assumption was disproved by live tests; use 0.2.7 strict mode for reviewed writes. The canonical
bundle format is unchanged.
Upgrade with:
python -m pip install --upgrade 'tinyhorse[gdrive]'
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 (or one folder inside a specified --container) 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 my-project
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, propose, accept, tag, projects, ingest_github, freeze_writer, strict_prepare, strict_publish).
projects discovers top-level Drive project folders plus one level inside configured
container folders (default PYPI DEV TINYHORSE PROJECTS) that are marked by Tiny Horse
or contain Git bundles, disambiguates duplicate names only when a single candidate is
verified by .tinyhorse.toml, 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. Ordinary Drive park uses a v3 resumable session and is not a verified concurrent-writer path. Opt-in strict mode places the reviewed ETag on a single v2 content PUT and never refreshes or retries the write. 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] [--container NAME]
tinyhorse park [PATH] [--force] # also: push
tinyhorse propose [PATH] [--id ID]
tinyhorse accept ID [PATH]
tinyhorse strict-prepare [PATH] --review-dir DIR
tinyhorse strict-publish DIR --review-sha256 SHA256
tinyhorse pull LOCATOR DESTINATION
tinyhorse doctor [PATH]
tinyhorse projects [--container NAME ...]
tinyhorse claim PROJECT [DESTINATION] [--container NAME ...] # 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] [--container NAME]
tinyhorse bundle PATH OUTPUT
tinyhorse verify-bundle BUNDLE
tinyhorse migrate REMOTE [DESTINATION] [--drive | --storage LOCATOR] [--project NAME] [--container 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 canonical bundle isolation matters
When live Git checkouts, old GitHub snapshots, and point-in-time backups sit together, an operator or 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.
Metadata
Release files for tinyhorse 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| tinyhorse-0.3.0.tar.gz | 133.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tinyhorse-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 200.8 kB
Release files / tinyhorse-0.3.0.tar.gz
| Download URL | tinyhorse-0.3.0.tar.gz |
|---|---|
| Size | 133.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
30b094e2cb0b4799dbd8ee824345167199bdf578d0cc505cee00c6249616160f
|
|
BLAKE2b-256 checksum How to use checksums |
39fc9ecc153a5c1e614905826592cf6c4245cdd0042ef07298e33c627ec696d5
|
| 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.3.0-py3-none-any.whl
| Download URL | tinyhorse-0.3.0-py3-none-any.whl |
|---|---|
| Size | 67.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1fff2f5b3024a615455829b3b7c2aa3d8347ac8b9f938812bb21ed078925ca32
|
|
BLAKE2b-256 checksum How to use checksums |
89b72d50a4a626ef02fd8fb1a4024e884f2a8abc4c1ae9b169f2d6e25a2bbd61
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.6
|
Security for tinyhorse 0.3.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.