A file-based ticket tracker, storing tickets as markdown with YAML frontmatter for AI-friendly, daemon-less workflows.
Clojure
2
355 commits
updated Sep 15, 2026
A CLI ticket tracker for solo developers.
Tickets are markdown files with YAML frontmatter under .tickets/.
Closed tickets auto-move to .tickets/archive/.
Built for one human on one machine, and for handing autonomous work to an AI agent.
See docs/prd/knot-v0.md for the full design rationale and the v0 acceptance criteria.
Knot is for solo developers who want tickets to live with the code, outside a hosted issue tracker or a hidden database. Each ticket is a markdown file you can edit, grep, diff, review, and commit with the work it describes.
It keeps the properties of a plain-file tracker (readable files, git-native history, no server)
and adds what a local, agent-driven workflow needs:
built-in JSON output, a configurable project schema, sortable collision-resistant IDs, dependency queries,
and an afk/hitl mode field that marks what an agent can run alone.
Knot is written in Clojure and ships as a single executable for Linux, macOS and Windows, but it can track work for any project: Clojure, JavaScript, Python, writing, infrastructure, or anything else you keep in a directory.
If you need multi-user coordination, permissions, dashboards, or hosted notifications, use an issue tracker. If you need a fast local backlog that an editor, shell, git, and agent can all understand, use Knot.
Each release publishes a knot executable for Linux and macOS (amd64 and aarch64) and for Windows (amd64).
It carries its own babashka runtime, so nothing else needs to be installed.
On Linux or macOS:
curl -fsSL https://raw.githubusercontent.com/UniSoma/knot/main/install.sh | sh
The script installs to ~/.local/bin/knot and warns if that directory is not on PATH.
On Windows, in PowerShell:
irm https://raw.githubusercontent.com/UniSoma/knot/main/install.ps1 | iex
The script installs to %LOCALAPPDATA%\Programs\knot\knot.exe and adds that directory to your user PATH.
Both scripts check the download against the release's SHA256SUMS.
Set KNOT_VERSION to install a specific release instead of the latest, and KNOT_INSTALL_DIR to install somewhere else:
curl -fsSL https://raw.githubusercontent.com/UniSoma/knot/main/install.sh | KNOT_VERSION=0.14.0 sh
Re-run the installer to upgrade.
Download knot-<os>-<arch>.tar.gz (or knot-windows-amd64.zip) and SHA256SUMS from the latest release, check the archive with sha256sum -c --ignore-missing SHA256SUMS (shasum -a 256 -c --ignore-missing SHA256SUMS on macOS), and put knot on your PATH.
The binaries are not signed. A browser download is quarantined, so clear the flag before the first run:
xattr -d com.apple.quarantine ./knot # macOS
Unblock-File .\knot.exe # Windows
If you already have babashka 1.3.0 or later, install with bbin:
bbin install https://github.com/UniSoma/knot.git
bbin reads :bbin/bin from bb.edn and drops a babashka shim on PATH.
bb bundles the runtime dependencies, so install resolves no :deps.
To install from a working copy during development:
bbin install . --as knot
To uninstall:
bbin uninstall knot
The install carries the agent-facing documentation with it.
knot help topics lists the bundled concept guides and knot help <topic> prints one.
knot skill install writes the same material into a project as an agent skill.
See AI-agent integration.
# Set up a project (writes .knot.edn stub + creates .tickets/)
knot init
# Create a ticket (defaults: type=task, priority=2, mode=hitl)
knot create "Fix login redirect"
knot create "Backfill telemetry" -d "Description body" --mode afk -p 1
# List the live (non-terminal) backlog
knot list # alias: `knot ls`
knot list --json # snake_case JSON for shell pipelines
# Inspect one ticket: frontmatter, body, computed graph context
knot show <id> # partial ids work: 01jq8p4a resolves to a unique full id
# Move work through the workflow
knot start <id> # to in_progress, bumps :updated
knot close <id> --summary "shipped in #482" # to closed + appended note
knot reopen <id> # restores from archive
# Acceptance criteria gate terminal transitions: knot refuses to close
# (or move active to terminal) while any --acceptance entry is unchecked.
# Mark them done with `knot update <id> --ac "<title>" --done`, or
# override the gate with `--force --summary "<reason>"`.
# Find what to pick up
knot ready # non-terminal + non-blocked, sorted by priority
knot ready --mode afk # only agent-runnable
knot blocked # tickets with at least one open dep
# Relationships
knot dep <from> <to> # cycle-checked dep add
knot link a b c # symmetric :links across every pair
knot dep tree <id> # ASCII tree, --full to expand dups
# Annotation
knot add-note <id> "raced GC under load"
knot edit <id> # opens $VISUAL/$EDITOR
knot update <id> --priority 0 --tags p0,auth # non-interactive frontmatter write
knot update <id> --description "New desc." # replace ## Description in place
knot update <id> --body "Plain body." # destructive whole-body replace
# Validate project integrity
knot check # cycles, dangling refs, schema, archive placement
knot check --code dep_cycle # filter by code (repeatable; OR within / AND across)
knot check --json # envelope + sorted issues, exit 0/1/2
Listing tables (ls, ready, blocked, closed) include a conditional AC column rendered as d/t (e.g. 2/5)
when any ticket in the result set has acceptance criteria.
Projects with no acceptance criteria get no column, so the table stays narrow.
Tickets without AC render as -.
ls --json passes the raw :acceptance list through.
Every read command (show, ls, ready, blocked, closed, dep tree, check, prime) accepts --json
and emits snake_case keys.
Stdout carries data only.
Warnings and errors go to stderr.
See the bundled skill's reference at
.claude/skills/knot/references/json.md
for the full envelope shape, per-command data payloads, and the error-code catalogue.
The reference travels with the skill,
so any project that installs the skill gets the protocol contract alongside it.
Listing commands (list / ls, ready, blocked, closed) emit ANSI color when stdout is a TTY.
Piping disables it.
Pass --no-color or set NO_COLOR to any non-empty value to force plain output,
following the no-color.org convention.
Knot treats NO_COLOR="" as unset.
.knot.edn schema.knot.edn at the project root is optional.
The defaults work with no config.
knot init writes a self-documenting stub with every key inline-commented.
| Key | Default | Notes |
|---|---|---|
:tickets-dir | ".tickets" | Where ticket files live, relative to .knot.edn. |
:prefix | auto-derived from project dir name | Project shortcode prefixed onto every generated id. |
:project-name | nil | Human-readable name shown in knot prime. |
:default-assignee | git user.name | Used when --assignee is omitted. When this key is set in .knot.edn (including to nil) it wins over git; nil opts out of auto-assignment entirely. |
:default-type | "task" | Must be a member of :types. |
:default-priority | 2 | Integer 0..4 (0 = highest). |
:statuses | ["open" "in_progress" "closed"] | Workflow, ordered. Add e.g. "review" to extend. |
:terminal-statuses | #{"closed"} | Tickets in these states auto-move to archive. |
:types | ["bug" "feature" "task" "epic" "chore"] | Allowed values for ticket :type. |
:modes | ["afk" "hitl"] | afk = agent-runnable; hitl = needs a human. |
:default-mode | "hitl" | Must be a member of :modes. |
:skill-dir | ".claude/skills/knot" | Where knot skill install writes the agent skill. Relative paths resolve from the project root; ~ expands. |
Knot finds the project root by walking up from cwd until it meets .knot.edn or .tickets/.
When it finds .knot.edn, that file's :tickets-dir controls where tickets live.
Knot warns on unknown keys and drops them.
Invalid values fail at command start.
For best results, configure three layers together so the agent recognizes the project, knows current state, and has the canonical reference on hand:
CLAUDE.md / AGENTS.md (~12 lines, always loaded),
which name knot as the tracker and forbid hand-editing .tickets/.knot prime, which injects live + ready tickets at the start of each session.knot skill install and loaded on demand by the agent.Each layer carries different content because each costs differently (per-turn tokens, per-session, on-demand).
Skip the skill and knot prime closes by pointing the agent at knot help topics instead,
which prints the same material a guide at a time.
Skip the project rules or the hook and Claude falls back to reading .tickets/ files directly,
which drifts from the CLI's invariants without warning.
knot prime emits a markdown primer of project state for injection into a fresh AI agent session.
It has a preamble with the canonical user-says intent table, project metadata,
in-progress tickets with a relative age column,
a Ready to close section listing active tickets whose every acceptance criterion is checked,
ready tickets (capped at 20 by default), and recently-closed tickets.
knot prime # markdown primer (project, in-progress, ready-to-close, ready, recently-closed)
knot prime --mode afk # filter ready section to agent-runnable work
knot prime --limit 5 # override the default ready cap of 20
knot prime --json # bare object with snake_case keys:
# project, in_progress, ready_to_close, ready,
# ready_truncated, ready_remaining, recently_closed,
# skill_installed, skill_dir
# JSON consumers should tolerate unknown keys;
# new ones may be added in future minor versions.
When any ticket in a section has acceptance criteria,
the row shape gains a conditional AC column rendered as d/t (e.g. 2/5) immediately before the title.
Sections with no AC tickets stay narrow.
The Ready to close section pairs with the --force --summary acceptance gate on terminal transitions.
It lists tickets where the gate would be a no-op (every AC is checked),
so the natural prompt is "close these before grabbing new work."
The staleness flag (stale: true, set when an in-progress ticket's :updated is 14+ days old)
appears only on in_progress entries.
A ticket may legitimately appear in both in_progress and ready (an in-progress ticket with all deps closed satisfies both),
and ready copies never carry stale.
To enumerate stalled work, iterate .in_progress and filter on stale.
The flag never appears on .ready.
knot prime always exits 0,
including when run from a directory with no Knot project (the preamble in that case directs the user to knot init),
when the project has zero tickets, or when only archived tickets exist.
That makes it safe to wire into a global session-start hook.
Configure your agent's session-start hook to run knot prime from the project.
The hook reads stdout and injects it as session context.
knot prime emits plain markdown, so the hook needs no JSON wrapper.
knot init does not modify agent configuration.
You set up the hook yourself.
For example, Claude Code users can add this to ~/.claude/settings.json (global)
or <project>/.claude/settings.json (project-local):
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "knot prime"
}
]
}
]
}
}
For agent-runnable session presets, run knot prime --mode afk to list only mode: afk ready tickets.
CLAUDE.md / AGENTS.md)Add a short block to your project's agent rules file so Claude (or another agent) recognizes the tracker on every turn,
and not only when an intent phrase happens to fire the skill.
The per-turn token cost buys protection against the most common failure:
Claude reaching for Read, Write, Edit, or grep against .tickets/ instead of the CLI.
Recommended snippet (paste into CLAUDE.md or AGENTS.md at the repo root):
## Issue tracking
This project tracks work with **knot**: markdown tickets under `.tickets/`
(closed auto-archive to `.tickets/archive/`), config in `.knot.edn` at the
repo root.
For any ticket-shaped intent ("what's next", "track this", "show me
<id>", "I'm done", "blocked on X", "any open bugs?"), use the `knot`
skill. It's the canonical reference. The CLI keeps frontmatter, the dep
graph, and the archive consistent, and resolves partial IDs across
live + archive.
**Never read or write `.tickets/` by hand.** No `Read`, `cat`, `grep`,
`Write`, `Edit`, `sed`, or `mv` against files in there. Use `knot
show` / `knot list --json` / `knot create` / `knot add-note` /
`knot update` / `knot close` instead. `knot edit` opens `$EDITOR`
(interactive); `knot update <id> --title ... --description ...
--json` is the non-interactive path agents and scripts should use.
If a knot command behaves unexpectedly, report the bug; don't
bypass.
Hosted-tracker prefixes (`GH-`, `ENG-`, `LIN-`, `JIRA-`) point at
*other* trackers. Use the matching tool, not knot.
Three things in this block earn the per-turn tokens.
The filesystem markers help the agent spot the project before any user intent fires.
The listed trigger phrases repeat keywords from the skill description.
The tool-named anti-pattern (Read, Write, Edit, cat, grep, sed, mv) matches the names of the agent's own tools.
The knot skill is the canonical reference: lifecycle gates, the dep graph, --json decision logic,
autonomous conduct, and the write paths.
Claude Code loads it on demand when triggers fire, at no per-turn cost.
The CLI carries the skill and writes it out on demand, so you need no clone of this repo:
knot skill install
That writes SKILL.md, references/ and agents/openai.yaml into .claude/skills/knot/ under the project root,
where Claude Code loads them from.
Pass a directory to install anywhere else.
knot skill install ~/.claude/skills/knot installs once for every project on the machine.
Set :skill-dir in .knot.edn to make a different directory the default.
Knot owns the files it writes and every install overwrites them. Re-running the command is how you take a newer version of the skill. Knot touches nothing else in the target directory.
The skill is plain markdown with nothing project-specific in it, so the same file works in every knot-tracked project.
The installed SKILL.md carries an <!-- installed by knot <version> --> comment, so you can tell which version a project holds.
The same files are the CLI's concept guides.
knot help topics lists them with a one-line summary each and knot help <topic> prints one to stdout,
so an agent with a shell reaches the reference in a project where nobody installed the skill.
SKILL.md and its five references/ files are one source, edited in one place and served both ways
(see ADR 0019).
Knot does no locking.
There is no .knot.lock, no fcntl, no MVCC, no optimistic-concurrency check.
Every write reads the ticket file, mutates it in memory, and writes it back.
The model assumes one writer per ticket at a time,
which is typical of a solo developer driving the CLI from a single shell,
or a single agent working through knot ready --mode afk end to end.
Git is the conflict-detection and undo path.
Tickets are plain markdown files committed alongside the code that motivates them,
so two writers who race to the same ticket produce a normal merge conflict at the next pull or rebase.
Treat git diff .tickets/ and git log -p .tickets/<id>--*.md as the authoritative history.
Nothing knot does is invisible to git.
There is also no force flag for destructive update --body rewrites, because git is the documented undo path there too.
If your workflow puts multiple writers on the same ticket concurrently,
for example parallel agents that pick up unblocked work without coordinating,
last-writer-wins will eventually drop a write.
The placeholder for an optimistic-concurrency check (read-modify-write with :updated as a CAS token) lives at
.tickets/kno-01kqgqaxzx98--future-work-optimistic-concurrency-control-via.md.
File a bug against your usage pattern there if you hit the issue in practice.
The design space is open until then.
Knot rests on a few assumptions. If your situation violates them, a different tool will serve you better.
knot show for reads and Knot commands for all writes
so computed graph context and frontmatter stay consistent.git add, git commit, or git push.
Your git log reflects your intent, and ticket changes bundle naturally with the code changes that motivate them..tickets/ (live + archive).
At personal scale, the cost is negligible and the simplicity wins..knot.lock files, no fcntl, no MVCC.
Two concurrent writers to the same ticket will produce the last-write-wins outcome you'd expect from a plain text file.
If you need multi-writer coordination, this is the wrong tool.knot show 01jq8p4a) keeps day-to-day typing short.:mode field (afk vs hitl) is a peer dimension to status and priority.
knot ready --mode afk lists unblocked, agent-runnable work in one query,
and knot prime keeps a fresh agent session oriented without any hand-written briefing.knot.ticket and knot.query are pure, and knot.store isolates filesystem I/O.
Future query layers can build on these namespaces without touching the CLI.MIT. See LICENSE.
355 commits
Clojure
88.2%
Emacs Lisp
11.1%
A file-based ticket tracker, storing tickets as markdown with YAML frontmatter for AI-friendly, daemon-less workflows.
Clojure
2
355 commits
updated Sep 15, 2026
A CLI ticket tracker for solo developers.
Tickets are markdown files with YAML frontmatter under .tickets/.
Closed tickets auto-move to .tickets/archive/.
Built for one human on one machine, and for handing autonomous work to an AI agent.
See docs/prd/knot-v0.md for the full design rationale and the v0 acceptance criteria.
Knot is for solo developers who want tickets to live with the code, outside a hosted issue tracker or a hidden database. Each ticket is a markdown file you can edit, grep, diff, review, and commit with the work it describes.
It keeps the properties of a plain-file tracker (readable files, git-native history, no server)
and adds what a local, agent-driven workflow needs:
built-in JSON output, a configurable project schema, sortable collision-resistant IDs, dependency queries,
and an afk/hitl mode field that marks what an agent can run alone.
Knot is written in Clojure and ships as a single executable for Linux, macOS and Windows, but it can track work for any project: Clojure, JavaScript, Python, writing, infrastructure, or anything else you keep in a directory.
If you need multi-user coordination, permissions, dashboards, or hosted notifications, use an issue tracker. If you need a fast local backlog that an editor, shell, git, and agent can all understand, use Knot.
Each release publishes a knot executable for Linux and macOS (amd64 and aarch64) and for Windows (amd64).
It carries its own babashka runtime, so nothing else needs to be installed.
On Linux or macOS:
curl -fsSL https://raw.githubusercontent.com/UniSoma/knot/main/install.sh | sh
The script installs to ~/.local/bin/knot and warns if that directory is not on PATH.
On Windows, in PowerShell:
irm https://raw.githubusercontent.com/UniSoma/knot/main/install.ps1 | iex
The script installs to %LOCALAPPDATA%\Programs\knot\knot.exe and adds that directory to your user PATH.
Both scripts check the download against the release's SHA256SUMS.
Set KNOT_VERSION to install a specific release instead of the latest, and KNOT_INSTALL_DIR to install somewhere else:
curl -fsSL https://raw.githubusercontent.com/UniSoma/knot/main/install.sh | KNOT_VERSION=0.14.0 sh
Re-run the installer to upgrade.
Download knot-<os>-<arch>.tar.gz (or knot-windows-amd64.zip) and SHA256SUMS from the latest release, check the archive with sha256sum -c --ignore-missing SHA256SUMS (shasum -a 256 -c --ignore-missing SHA256SUMS on macOS), and put knot on your PATH.
The binaries are not signed. A browser download is quarantined, so clear the flag before the first run:
xattr -d com.apple.quarantine ./knot # macOS
Unblock-File .\knot.exe # Windows
If you already have babashka 1.3.0 or later, install with bbin:
bbin install https://github.com/UniSoma/knot.git
bbin reads :bbin/bin from bb.edn and drops a babashka shim on PATH.
bb bundles the runtime dependencies, so install resolves no :deps.
To install from a working copy during development:
bbin install . --as knot
To uninstall:
bbin uninstall knot
The install carries the agent-facing documentation with it.
knot help topics lists the bundled concept guides and knot help <topic> prints one.
knot skill install writes the same material into a project as an agent skill.
See AI-agent integration.
# Set up a project (writes .knot.edn stub + creates .tickets/)
knot init
# Create a ticket (defaults: type=task, priority=2, mode=hitl)
knot create "Fix login redirect"
knot create "Backfill telemetry" -d "Description body" --mode afk -p 1
# List the live (non-terminal) backlog
knot list # alias: `knot ls`
knot list --json # snake_case JSON for shell pipelines
# Inspect one ticket: frontmatter, body, computed graph context
knot show <id> # partial ids work: 01jq8p4a resolves to a unique full id
# Move work through the workflow
knot start <id> # to in_progress, bumps :updated
knot close <id> --summary "shipped in #482" # to closed + appended note
knot reopen <id> # restores from archive
# Acceptance criteria gate terminal transitions: knot refuses to close
# (or move active to terminal) while any --acceptance entry is unchecked.
# Mark them done with `knot update <id> --ac "<title>" --done`, or
# override the gate with `--force --summary "<reason>"`.
# Find what to pick up
knot ready # non-terminal + non-blocked, sorted by priority
knot ready --mode afk # only agent-runnable
knot blocked # tickets with at least one open dep
# Relationships
knot dep <from> <to> # cycle-checked dep add
knot link a b c # symmetric :links across every pair
knot dep tree <id> # ASCII tree, --full to expand dups
# Annotation
knot add-note <id> "raced GC under load"
knot edit <id> # opens $VISUAL/$EDITOR
knot update <id> --priority 0 --tags p0,auth # non-interactive frontmatter write
knot update <id> --description "New desc." # replace ## Description in place
knot update <id> --body "Plain body." # destructive whole-body replace
# Validate project integrity
knot check # cycles, dangling refs, schema, archive placement
knot check --code dep_cycle # filter by code (repeatable; OR within / AND across)
knot check --json # envelope + sorted issues, exit 0/1/2
Listing tables (ls, ready, blocked, closed) include a conditional AC column rendered as d/t (e.g. 2/5)
when any ticket in the result set has acceptance criteria.
Projects with no acceptance criteria get no column, so the table stays narrow.
Tickets without AC render as -.
ls --json passes the raw :acceptance list through.
Every read command (show, ls, ready, blocked, closed, dep tree, check, prime) accepts --json
and emits snake_case keys.
Stdout carries data only.
Warnings and errors go to stderr.
See the bundled skill's reference at
.claude/skills/knot/references/json.md
for the full envelope shape, per-command data payloads, and the error-code catalogue.
The reference travels with the skill,
so any project that installs the skill gets the protocol contract alongside it.
Listing commands (list / ls, ready, blocked, closed) emit ANSI color when stdout is a TTY.
Piping disables it.
Pass --no-color or set NO_COLOR to any non-empty value to force plain output,
following the no-color.org convention.
Knot treats NO_COLOR="" as unset.
.knot.edn schema.knot.edn at the project root is optional.
The defaults work with no config.
knot init writes a self-documenting stub with every key inline-commented.
| Key | Default | Notes |
|---|---|---|
:tickets-dir | ".tickets" | Where ticket files live, relative to .knot.edn. |
:prefix | auto-derived from project dir name | Project shortcode prefixed onto every generated id. |
:project-name | nil | Human-readable name shown in knot prime. |
:default-assignee | git user.name | Used when --assignee is omitted. When this key is set in .knot.edn (including to nil) it wins over git; nil opts out of auto-assignment entirely. |
:default-type | "task" | Must be a member of :types. |
:default-priority | 2 | Integer 0..4 (0 = highest). |
:statuses | ["open" "in_progress" "closed"] | Workflow, ordered. Add e.g. "review" to extend. |
:terminal-statuses | #{"closed"} | Tickets in these states auto-move to archive. |
:types | ["bug" "feature" "task" "epic" "chore"] | Allowed values for ticket :type. |
:modes | ["afk" "hitl"] | afk = agent-runnable; hitl = needs a human. |
:default-mode | "hitl" | Must be a member of :modes. |
:skill-dir | ".claude/skills/knot" | Where knot skill install writes the agent skill. Relative paths resolve from the project root; ~ expands. |
Knot finds the project root by walking up from cwd until it meets .knot.edn or .tickets/.
When it finds .knot.edn, that file's :tickets-dir controls where tickets live.
Knot warns on unknown keys and drops them.
Invalid values fail at command start.
For best results, configure three layers together so the agent recognizes the project, knows current state, and has the canonical reference on hand:
CLAUDE.md / AGENTS.md (~12 lines, always loaded),
which name knot as the tracker and forbid hand-editing .tickets/.knot prime, which injects live + ready tickets at the start of each session.knot skill install and loaded on demand by the agent.Each layer carries different content because each costs differently (per-turn tokens, per-session, on-demand).
Skip the skill and knot prime closes by pointing the agent at knot help topics instead,
which prints the same material a guide at a time.
Skip the project rules or the hook and Claude falls back to reading .tickets/ files directly,
which drifts from the CLI's invariants without warning.
knot prime emits a markdown primer of project state for injection into a fresh AI agent session.
It has a preamble with the canonical user-says intent table, project metadata,
in-progress tickets with a relative age column,
a Ready to close section listing active tickets whose every acceptance criterion is checked,
ready tickets (capped at 20 by default), and recently-closed tickets.
knot prime # markdown primer (project, in-progress, ready-to-close, ready, recently-closed)
knot prime --mode afk # filter ready section to agent-runnable work
knot prime --limit 5 # override the default ready cap of 20
knot prime --json # bare object with snake_case keys:
# project, in_progress, ready_to_close, ready,
# ready_truncated, ready_remaining, recently_closed,
# skill_installed, skill_dir
# JSON consumers should tolerate unknown keys;
# new ones may be added in future minor versions.
When any ticket in a section has acceptance criteria,
the row shape gains a conditional AC column rendered as d/t (e.g. 2/5) immediately before the title.
Sections with no AC tickets stay narrow.
The Ready to close section pairs with the --force --summary acceptance gate on terminal transitions.
It lists tickets where the gate would be a no-op (every AC is checked),
so the natural prompt is "close these before grabbing new work."
The staleness flag (stale: true, set when an in-progress ticket's :updated is 14+ days old)
appears only on in_progress entries.
A ticket may legitimately appear in both in_progress and ready (an in-progress ticket with all deps closed satisfies both),
and ready copies never carry stale.
To enumerate stalled work, iterate .in_progress and filter on stale.
The flag never appears on .ready.
knot prime always exits 0,
including when run from a directory with no Knot project (the preamble in that case directs the user to knot init),
when the project has zero tickets, or when only archived tickets exist.
That makes it safe to wire into a global session-start hook.
Configure your agent's session-start hook to run knot prime from the project.
The hook reads stdout and injects it as session context.
knot prime emits plain markdown, so the hook needs no JSON wrapper.
knot init does not modify agent configuration.
You set up the hook yourself.
For example, Claude Code users can add this to ~/.claude/settings.json (global)
or <project>/.claude/settings.json (project-local):
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "knot prime"
}
]
}
]
}
}
For agent-runnable session presets, run knot prime --mode afk to list only mode: afk ready tickets.
CLAUDE.md / AGENTS.md)Add a short block to your project's agent rules file so Claude (or another agent) recognizes the tracker on every turn,
and not only when an intent phrase happens to fire the skill.
The per-turn token cost buys protection against the most common failure:
Claude reaching for Read, Write, Edit, or grep against .tickets/ instead of the CLI.
Recommended snippet (paste into CLAUDE.md or AGENTS.md at the repo root):
## Issue tracking
This project tracks work with **knot**: markdown tickets under `.tickets/`
(closed auto-archive to `.tickets/archive/`), config in `.knot.edn` at the
repo root.
For any ticket-shaped intent ("what's next", "track this", "show me
<id>", "I'm done", "blocked on X", "any open bugs?"), use the `knot`
skill. It's the canonical reference. The CLI keeps frontmatter, the dep
graph, and the archive consistent, and resolves partial IDs across
live + archive.
**Never read or write `.tickets/` by hand.** No `Read`, `cat`, `grep`,
`Write`, `Edit`, `sed`, or `mv` against files in there. Use `knot
show` / `knot list --json` / `knot create` / `knot add-note` /
`knot update` / `knot close` instead. `knot edit` opens `$EDITOR`
(interactive); `knot update <id> --title ... --description ...
--json` is the non-interactive path agents and scripts should use.
If a knot command behaves unexpectedly, report the bug; don't
bypass.
Hosted-tracker prefixes (`GH-`, `ENG-`, `LIN-`, `JIRA-`) point at
*other* trackers. Use the matching tool, not knot.
Three things in this block earn the per-turn tokens.
The filesystem markers help the agent spot the project before any user intent fires.
The listed trigger phrases repeat keywords from the skill description.
The tool-named anti-pattern (Read, Write, Edit, cat, grep, sed, mv) matches the names of the agent's own tools.
The knot skill is the canonical reference: lifecycle gates, the dep graph, --json decision logic,
autonomous conduct, and the write paths.
Claude Code loads it on demand when triggers fire, at no per-turn cost.
The CLI carries the skill and writes it out on demand, so you need no clone of this repo:
knot skill install
That writes SKILL.md, references/ and agents/openai.yaml into .claude/skills/knot/ under the project root,
where Claude Code loads them from.
Pass a directory to install anywhere else.
knot skill install ~/.claude/skills/knot installs once for every project on the machine.
Set :skill-dir in .knot.edn to make a different directory the default.
Knot owns the files it writes and every install overwrites them. Re-running the command is how you take a newer version of the skill. Knot touches nothing else in the target directory.
The skill is plain markdown with nothing project-specific in it, so the same file works in every knot-tracked project.
The installed SKILL.md carries an <!-- installed by knot <version> --> comment, so you can tell which version a project holds.
The same files are the CLI's concept guides.
knot help topics lists them with a one-line summary each and knot help <topic> prints one to stdout,
so an agent with a shell reaches the reference in a project where nobody installed the skill.
SKILL.md and its five references/ files are one source, edited in one place and served both ways
(see ADR 0019).
Knot does no locking.
There is no .knot.lock, no fcntl, no MVCC, no optimistic-concurrency check.
Every write reads the ticket file, mutates it in memory, and writes it back.
The model assumes one writer per ticket at a time,
which is typical of a solo developer driving the CLI from a single shell,
or a single agent working through knot ready --mode afk end to end.
Git is the conflict-detection and undo path.
Tickets are plain markdown files committed alongside the code that motivates them,
so two writers who race to the same ticket produce a normal merge conflict at the next pull or rebase.
Treat git diff .tickets/ and git log -p .tickets/<id>--*.md as the authoritative history.
Nothing knot does is invisible to git.
There is also no force flag for destructive update --body rewrites, because git is the documented undo path there too.
If your workflow puts multiple writers on the same ticket concurrently,
for example parallel agents that pick up unblocked work without coordinating,
last-writer-wins will eventually drop a write.
The placeholder for an optimistic-concurrency check (read-modify-write with :updated as a CAS token) lives at
.tickets/kno-01kqgqaxzx98--future-work-optimistic-concurrency-control-via.md.
File a bug against your usage pattern there if you hit the issue in practice.
The design space is open until then.
Knot rests on a few assumptions. If your situation violates them, a different tool will serve you better.
knot show for reads and Knot commands for all writes
so computed graph context and frontmatter stay consistent.git add, git commit, or git push.
Your git log reflects your intent, and ticket changes bundle naturally with the code changes that motivate them..tickets/ (live + archive).
At personal scale, the cost is negligible and the simplicity wins..knot.lock files, no fcntl, no MVCC.
Two concurrent writers to the same ticket will produce the last-write-wins outcome you'd expect from a plain text file.
If you need multi-writer coordination, this is the wrong tool.knot show 01jq8p4a) keeps day-to-day typing short.:mode field (afk vs hitl) is a peer dimension to status and priority.
knot ready --mode afk lists unblocked, agent-runnable work in one query,
and knot prime keeps a fresh agent session oriented without any hand-written briefing.knot.ticket and knot.query are pure, and knot.store isolates filesystem I/O.
Future query layers can build on these namespaces without touching the CLI.MIT. See LICENSE.
355 commits
Clojure
88.2%
Emacs Lisp
11.1%