ticgit: an issue tracker that lives in your Git repository
Git based distributed ticketing system, built on git-meta
At a glance
- What is it?
- ticgit stores tickets as structured metadata in the repository itself rather than in a service, so the issue tracker is versioned, diffable and synchronised with the same push and pull you already use for code, and it ships a JSON schema aimed squarely at agents.
- Who is it for?
- ticgit fits a team that already keeps its code in Git and wants the issue tracker in the same place, with the same history and the same merge conflicts, and it fits anyone building agent workflows who needs a stable machine interface rather than a screen to scrape. It does not fit a team that wants a hosted tracker with a web UI, a notification pipeline and per-field permissions, since this is a command line tool with a TUI and a JSON schema.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 79 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The idea: tickets as metadata in the repository
ticgit is a Git-native issue tracker where tickets live in the repository as structured git-meta metadata, and the ti command line tool can create, read, update and sync ticket data. The storage layer is delegated to the git-meta library rather than invented from scratch, and the consequence is that the whole tracker inherits Git's properties. There is no separate database to back up, no service to run, and no second place where the history of a decision lives, since a ticket change is a change in the repository and shows up in a diff like anything else. The primary language is Rust, the licence is MIT, the project is at ticgit.dev, and the release history shows v0.3.1 from 2026-05-20, v0.3.0 the day before and v0.2.2 a few days before that. The repository is the workspace of Scott Chacon, whose name appears in the manifest as the author.
Installing it, and the first five commands
There is a pre-built binary, installed with a shell pipe.
curl -fsSL https://ticgit.dev/install | shOr you can install from source with Cargo.
cargo install ticgitThe binary is named ti. The quick start then makes a repository, sets a user identity, initialises the tracker and creates a first ticket.
git init
git config user.email [email protected]
git config user.name "Your Name"
ti init
ti new --title "fix the parser" --tags bug,parser --comment "fails on empty input"
ti list
ti show <ticket-id-or-prefix>Most commands accept a full UUID or any unique UUID prefix, which is the detail that makes the tool pleasant to use by hand, since you can type the first few characters of an identifier rather than copying a whole one.
Creating and finding tickets
Creating a ticket takes a title, and optionally tags, an assignee and a comment.
ti new --title "add docs"
ti new --title "fix crash" --tags bug,cli --assigned [email protected]
ti new --title "investigate flaky test" --comment "seen on CI twice"Listing is where the filters live, and the set of them is the query language.
ti list
ti list --status open
ti list --state blocked
ti list --tag bug
ti list --assigned [email protected]
ti list --order title.desc
ti list --json
ti list --markdownThe distinction between status and state comes up here and it is worth understanding early. Lifecycle values are split into a broad status and a specific state. Open tickets use new, assigned, in-progress, blocked, or review, and closed tickets use resolved, wontfix, duplicate, or invalid. New tickets start as open:new, and ti state closed defaults to closed:resolved. So a status tells you whether the work is live, and a state tells you what happened to it, and the pair is what gets stored.
Mutating a ticket, and working on a current one
The mutation commands are all of the same shape, a verb plus a ticket selector, and the selector is a flag rather than a positional argument on most of them.
ti state blocked --ticket <id>
ti state closed --ticket <id>
ti state closed:wontfix --ticket <id>
ti status review --ticket <id>
ti assign [email protected] --ticket <id>
ti assign --clear --ticket <id>
ti points 3 --ticket <id>
ti milestone v1.0 --ticket <id>
ti tag --ticket <id> bug ui
ti tag --ticket <id> --remove ui
ti edit <id>
ti comment --ticket <id> "fixed in the latest patch"Alongside that there is a notion of a current ticket, set by checkout, which is how a single-workflow session works.
ti checkout <id>
ti show
ti comment "follow-up note"
ti checkout --clearAnd a recent list for picking up where you left off.
ti recent
ti recent --limit 20Saved views are named snapshots of ticket UUIDs, so a view is a list you can pin rather than a query that changes under you.
ti save-view bugs --tag bug
ti views
ti views bugs
ti list --view bugsThe machine interface, which is the part aimed at agents
Everything has --json output for scripting and --markdown output for agentic use, and the readme is explicit that commands supporting --json also support --markdown, which renders the same ticket data as Markdown and includes suggested next commands for agent workflows. The JSON side is governed by a stable schema published at docs/schema/v1.json and, on the website, at the same path under the site domain, and the rules are strict enough to be worth quoting. Successful JSON commands write parseable JSON to stdout only, diagnostic and error text goes to stderr, JSON output does not include ANSI color escapes, and a non-zero exit status means the command failed. Ticket ids may be full UUIDs or unique UUID prefixes, and an ambiguous or missing prefix fails with a non-zero exit status and a stderr diagnostic.
There is one compatibility note, and it is a warning rather than an option. The --porcelain and --format json flags are not supported compatibility aliases today, so you should use --json for schema-stable output. The shape of the payload is simple too. ti show with --json and the JSON mutation commands emit a ticket object, ti list --json emits an array of ticket objects, and ticket metadata appears under .meta as an object whose values are strings. Agents can run ti help --agent for a Markdown guide, or read the website's Markdown version at docs/index.md.
What gets stored, and how it moves between clones
All the data is written on the git-meta project target under the ticgit namespace, and the key list is short enough to read as the whole schema.
ticgit:schema-version string
ticgit:owners set
ticgit:views:<name> set of ticket UUIDs
ticgit:tickets:<uuid>:title string
ticgit:tickets:<uuid>:description string (optional)
ticgit:tickets:<uuid>:status string
ticgit:tickets:<uuid>:state string
ticgit:tickets:<uuid>:assigned string
ticgit:tickets:<uuid>:points string
ticgit:tickets:<uuid>:milestone string
ticgit:tickets:<uuid>:tags set
ticgit:tickets:<uuid>:comments list
ticgit:tickets:<uuid>:created-at string
ticgit:tickets:<uuid>:created-by stringTicket existence is implied by the presence of fields under ticgit:tickets:<uuid>:*, and there is no separate ticket index, which is a neat consequence of using a metadata store rather than a table. The local query database is git-meta's .git/git-meta.sqlite, and exchange with other clones happens through refs/meta using normal Git transfer. Sync is delegated to git-meta-lib as well, with a pull, a push and a sync that does both.
ti pull
ti push
ti syncIf you pass --remote <name> the named git-meta remote is used, and otherwise git-meta resolves the default metadata remote from Git config. There is also an import path for existing GitHub issues.
ti import gh
ti import gh --repo owner/repoThe Rust API and the workspace layout
The workspace has two crates. ticgit-lib is the domain model and the git-meta-backed TicketStore, and ticgit is the ti command line application, and the library is a thin layer over the metadata store rather than a separate persistence model. The example is short enough to show the whole shape of the API.
use ticgit_lib::{NewTicketOpts, TicketStore};
let store = TicketStore::discover()?;
let ticket = store.create("fix parser", NewTicketOpts::default())?;
println!("{}", ticket.id);
Ok::<(), ticgit_lib::Error>(())The store is discovered rather than configured, which means the library finds the repository the way Git does. The dependency list in the workspace manifest shows the stack. Git work is done through gix with default features off and a specific set enabled, sha1, revision, merge, tree-editor and blob-diff, and gix-actor is pinned exactly to 0.40.0 because a later bump moved winnow to 1.0 while gix-object 0.58 still needs 0.7. The interface is clap with derive, the terminal interface is ratatui with crossterm, and syntax highlighting comes from syntect. There is also a TUI, reachable as ti tui, which the readme presents alongside the CLI.
Building, testing and publishing from source
The development commands are the standard Cargo ones, split by what you want to run.
cargo testcargo test -p ticgit-libcargo test -p ticgit --test cliAnd the CLI builds on its own with this.
cargo build -p ticgitPublishing has an ordering constraint that is easy to miss. The CLI crate depends on ticgit-lib by both a local path and a published version, so you publish ticgit-lib first.
cargo package -p ticgit-lib
cargo publish -p ticgit-lib
# After ticgit-lib 0.1.0 is available in the crates.io index:
cargo package -p ticgit
cargo publish -p ticgitThe repository also carries a .git-meta directory at the top level, an AGENTS.md, a docs directory and a scripts directory, which is the shape you would expect from a project that is meant to be driven as much by people as by agents.
Editorial conclusion
ticgit fits a team that already keeps its code in Git and wants the issue tracker in the same place, with the same history and the same merge conflicts, and it fits anyone building agent workflows who needs a stable machine interface rather than a screen to scrape. It does not fit a team that wants a hosted tracker with a web UI, a notification pipeline and per-field permissions, since this is a command line tool with a TUI and a JSON schema. It also does not fit a user who wants to work offline across machines without any remote, because the exchange between clones happens through refs/meta using normal Git transfer. Before you adopt it, read the machine output rules, since the split between stdout and stderr and the promise that JSON carries no ANSI escapes are what make it scriptable, and use --json rather than the older compatibility aliases, which are not supported.
Frequently asked questions
How do I install ticgit and create my first ticket?
Download a pre-built binary with curl -fsSL https://ticgit.dev/install piped to sh, or run cargo install ticgit. The binary is named ti. Then run git init, set the user email and name, run ti init, and create a ticket with ti new, giving it a title and optionally tags, an assignee and a comment.
How does ticgit store tickets and sync them between clones?
All data is written on the git-meta project target under the ticgit namespace, with keys such as ticgit:tickets:<uuid>:title, and ticket existence is implied by those fields rather than by a separate index. The local query database is git-meta's .git/git-meta.sqlite, and exchange between clones happens through refs/meta using normal Git transfer, driven by ti pull, ti push and ti sync.
What is the difference between status and state in ticgit?
Lifecycle values are split into a broad status and a specific state. Open tickets use new, assigned, in-progress, blocked or review, and closed tickets use resolved, wontfix, duplicate or invalid. New tickets start as open:new, and ti state closed defaults to closed:resolved.
How do I get JSON output from ticgit for automation?
Use the --json flag, which is the stable machine interface with a published schema at docs/schema/v1.json. Successful commands write parseable JSON to stdout only, diagnostics go to stderr, no ANSI color escapes appear in the output, and a non-zero exit status means failure. The older --porcelain and --format json flags are not supported aliases today.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/schacon-ticgit)