Command reference
knotless
Section titled “knotless”<url> is a repository’s knotless://<handle or did>/<name>. A trailing
/ and a trailing .git are dropped, and any other / in the name is
refused. A name is 1 to 100 bytes of [A-Za-z0-9._~-], not . or ...
<at-uri> is a pull’s, issue’s or comment’s at:// URI, as pull,
issue, list and show print them.
An error prints knotless: <error> and exits 1. One a PDS refused for want
of scope, as a login from before knotless asked for more is, adds a second
line, knotless: your login is older than what knotless asks for now: run knotless login again to allow this. Until then, any command but login
and logout ends by printing to stderr knotless: your login is older than what knotless asks for now: run knotless login again to allow all of it, whether or not it failed.
Once a day, a command asks KNOTLESS_APPVIEW for its newest release
(/releases/latest) and the oldest version it asks clients to be
(dev.knotless.describeServer’s client), waiting 3 s at most, and keeps
what it said in update.json; an appview that does not answer leaves
what was known. With a newer release out, a command ends by printing to
stderr knotless: knotless <latest> is out, and this is <version>. Run knotless upgrade to install it. Older than the appview asks for, any
command but upgrade, login, logout and help fails at once, saying
knotless <version> is older than the <minimum> the appview asks for. <reason> Run knotless upgrade, or set KNOTLESS_IGNORE_MINIMUM=1 to go on anyway. KNOTLESS_NO_UPDATE_CHECK turns both off. knotless-mcp over
stdio does the same: with a newer release out, its first tool’s answer
ends with that line, and older than asked for, every tool fails with that
refusal.
Accounts
Section titled “Accounts”login <handle> [--password-stdin]log in with OAuth in a browser, or with an app password read from standard input (or
KNOTLESS_PASSWORD)logout <handle>drop the session, revoking its tokens
upgradeinstall the newest release over this one, running the appview’s
/install.shintoKNOTLESS_INSTALL_DIR, or else the directory this binary is in, and printing what it said (knotless: installing <version> for <target> into <dir>). Refused where a package manager put it:~/.cargo/bin(cargo installed knotless here: upgrade it with cargo), a HomebrewCellaror/usr/bin. With no appview, failsan appview hands out releases, and none is set: set KNOTLESS_APPVIEW; with one that hands out none,<appview> hands out no releasesdelete-data <handle> [--yes]delete every knotless record from your account (storage.md): prints your repositories, one line each,
repository <name>, then(private)and, shared with <n>where they apply, then each collection’s count and name, tab-separated, and<n> spaces; asks you to type the handle again unless--yes, and refuses anything else (nothing deleted: …); then prints<n> deletedas it goes, has the appview forget you, and printsdeleted <n>, and logged out. Run again, one cut short carries on. An OAuth login from before this asks for it with a new scope: log in againemailhow many addresses you claim:
<n> addresses claimed. Each is kept only as a hash salted with your DID, so none can be listedemail add <address>/email remove <address>claim, or take back, an address your commits are authored as, printing
<address> added,removedorunchanged; one with no@fails"<address>" is no email addressprofile [--name <text>] [--bio <text>] [--pronouns <text>] [--link <url> [--title <text>]]... [--avatar <file>] [--bot | --no-bot]how you show on knotless, over your Bluesky profile;
''shows Bluesky’s field again, and--link ''its links; the avatar is a PNG, JPEG, WebP or GIF, made smaller when over 1 MB;--botsays the account is a bot, which its profile marks and the feed leaves out, printed asbot: yes, and--no-bottakes that back
Repositories
Section titled “Repositories”create <url> [--private] [--description <text>] [--link <url> [--title <text>]]... [--default-branch <name>] [--runner <handle>]... [--no-runner]create an empty repository; the default branch is
mainunless named; each--link, an http or https address, is one its pages show, in order, its website first, up to five, called--titleif one follows it;<url> already existsif it does. Itsrunnersare those--runnernames, none with--no-runner, and otherwise those the appview names for new repositories (dev.knotless.describeServer); a repository a first push makes names those too. A private one’s are made members of it and invited at the appview, so that they find it, each printed as<handle> reads it, to run its checks: …; one that could not be invited printswarning: <did> was not told of it: <why>. In an organization, says how many of its members may push to it: on a public one, its public members, as a hidden member’s merges count only where readers can see them. An admin of an organization makes one in it by naming it in<url>, through its gateway; forking, moving or linking one there takes the organization’s own loginls <handle>list an account’s repositories
refs <url>print the refs, in one round trip
search <url> <pattern> [--rev <rev>] [--limit <n>]the lines holding a fixed string, in any case, at the default branch or
<rev>, at most<n>(100 by default), aspath:line:text, read from the cache mirror with no clone; an empty pattern failsan empty searchblame <url> <path> [--rev <rev>]who last changed each line of a file, at the default branch or
<rev>, read from the cache mirror with no clone: a line for each run of lines one commit last changed, as<first>-<last>, the commit, its author’s name, when it was written (UTC) and its subject, tab-separateddiff <url> <at-uri>the unified diff a pull would apply: its head, in whichever repository it is, against where it left its base
diff <url> <base> <head>what
<head>changed since it forked from<base>, asgit diff base...headshows it, with no clonecommit <url> -b <branch> -m <message> [options]commit without a checkout (below)
branch <url> <name> [<from>]create a branch at the default branch, or at a branch or commit, and print its commit;
<url> already has a branch <name>if it existsbranch <url> <name> --deletedelete a branch; refuses the default branch (
<name> is <url>'s default branch) and one that does not exist (<url> has no branch <name>)branch <url> <name> --rename <new name>rename a branch, printing
renamed <name> to <new name>: the new one starts where the old one is, each open pull into it is pointed at the new name, printedretargeted <at-uri>, and the old one is deleted. Refuses the default branch. Needs an appview, which lists the pullstag <url> <name> [<from>]tag the default branch’s commit, or that of a branch, tag or commit, and print it;
<url> already has a tag <name>if it exists. A lightweight tag, naming the committag <url> <name> --deletedelete a tag, printing
deleted <name>;<url> has no tag <name>if there is nonefork <url> [<name>]copy a repository into your account, under its name unless one is given; one’s own fails
you own <url>: a fork is of someone else's repository, and a name you already have fails<url> already existsstar <url>star a public repository, printing the star’s
at://URI; one already starred keeps its star; a private one fails<url>: a private repository can't be starred, as a star is publicunstar <url>take your star back, printing
unstarred, starred or notstars [<handle>]the repositories you, or
<handle>, starred, newest first:knotless://<handle>/<name>and when, tab-separatednotifications [--peek] [--kind <kind>]what others did that concerns you, the latest 50, from the appview:
new(or nothing, once seen), when, why (star,follow,pull,issue: on a repository you own, collaborate on, or that an organization you joined owns;comment: on your pull or issue, or one you commented on; for a review,approveorchanges;mention: a pull, issue or comment that names you as@handle;invite: an organization’s invitation you have yet to answer, with the role it offers, asas admin;checks-failed: checks failing at your pull’s latest run, andejected: the merge queue taking your pull out, each from the runner, with the checks that failed, asfailed test, web), who, and the repository and title, tab-separated; marks them seen unless--peek.--kindlists onlymentions,reviews,comments,pulls-and-issues,checksorstars-and-follows, and marks none seen, as that would mark seen those of other kinds before them. With no appview, failsnotifications come from an appview, and none is set: set KNOTLESS_APPVIEWnotifications settings [--on <reason>]... [--off <reason>]... [--followed-only | --anyone]which notifications you hear of, kept by the appview: each reason turned on or off, and with
--followed-onlyonly of what accounts you follow did (--anyoneundoes it). Prints every reason the appview gives,<reason>, a tab andonoroff, thenfrom, a tab andanyoneoraccounts you follow; with no flags, only prints them. A reason the appview does not give fails"<reason>" is no reason: star, follow, ….notifications, MCP and the pages leave out what you turned off. An OAuth login from before this asks for it with a new scope: log in againfollow <handle>follow an account on knotless (a
dev.knotless.follow, not a Bluesky follow), printing the follow’sat://URI; one already followed keeps its follow; yourself failsyou can't follow yourselfunfollow <handle>take your follows of it back, printing
unfollowed, followed or notfollows [<handle>]the accounts you, or
<handle>, follow, newest first: handle and when, tab-separatedthemes [<handle>] [--find <name>]themes made for the pages, from the appview (
dev.knotless.listThemes), the latest 100: everyone’s, or<handle>’s, or with--findthose whose name starts with it, in any case, by name. Each its name, its maker’s handle and itsat://URI, tab-separated; with no appview, failsthemes are listed and chosen by an appview: set KNOTLESS_APPVIEWtheme check <file>whether every text of a palette reads at 4.5 to 1 on what it is drawn on, as the pages hold their own themes: prints
every text reads at 4.5 to 1 or more, or failssome text would be hard to read, each needing 4.5 to 1: textMuted on background is 1.2 to 1; …, each named as the palette names it, or as the pages name a colour they mix from it.<file>is JSON, adev.knotless.theme#paletteor a record holding one;-reads stdintheme publish <name> <file> [--use]publishes the palette, once checked as
theme checkdoes, as adev.knotless.themecalled<name>(1 to 64 characters), for anyone to choose, printing itsat://URI;--usedraws your pages in it astheme usedoestheme use <theme>draws your pages in a theme, kept by the appview with your preferences, so the pages take it as you next log in to them:
system,light,dark,latte,mocha,dracula,pckt,plyr,blacksky, one published, byat://URI, whose colours are copied, or a palette of your own, unpublished, from a.jsonfile or-, checked first. Printsdrawn in <name>theme delete <uri>deletes a theme you published, printing
deleted; those who chose it keep its colours. One not yours fails<uri> is no theme of yoursfollowers [<handle>]the accounts following you, or
<handle>, newest first, a handle a line, as the appview counts them; with no appview, failsan appview counts followers, and none is set: set KNOTLESS_APPVIEWdescribe <url> [--description <text>] [--link <url> [--title <text>]]... [--topic <name>]... [--default-branch <name>]as the owner, change what a repository says it is (
''drops it), the links its pages show (the--links given replace those it had, asprofile’s do, and--link ''drops them), its topics (the--topics given replace those it had, each lowercased and named once, of letters, digits and dashes, at most 50 long and 20 in all;--topic ''drops them), and the branch its HEAD names, one of its branches once it has any, printingdescription: <text>,default branch: <name>,topics: <name>, …when it has any, and alink: <url> (<title>)line for each linkowners <text> [--limit <n>]the accounts owning public repositories whose handle holds
<text>, in any case, as the search on the appview’s pages finds them (dev.knotless.searchOwners): those whose handle starts with it first, at most<n>(10 by default, at most 50), each handle and how many public repositories it owns, tab-separated; with no appview, failsan appview finds accounts, and none is set: set KNOTLESS_APPVIEWarchive <url> [--undo]as the owner, make a repository read only, printing
archived <url> at <time>, or with--undowritable again, printingunarchived <url>. While it is archived, pushes and commits to it are refused, byknotlessand by the gateway, and so are pulls, issues and comments about it, each with<name> is archived and read only. Its owner can unarchive it.; runners leave it. One already as asked fails<url> is already archived(ornot archived)rename <url> <new name>rename; the old name redirects;
<new url> already existsif another repository has the nametransfer <url> <handle or url>/transfer <url> --canceloffer a public repository to another account, or withdraw the offer
accept <url>take a repository offered to you, its pulls and issues included
delete <url> --yesdelete a repository and everything in it: its records, and what you wrote about it (your pulls, issues, comments, labels, star and reactions on it); run again, a delete cut short carries on
compact <url>as the owner, fold its packs into one, printing
<n> packs, <n> folded, <n> old claims droppedstorage <url> [--json]what it takes up on its PDS, from the pack records alone:
<n> packs, <n> objects, <size>, <size> in blobs, or{packs, objects, bytes, blobBytes}mirror <url>sync the cache mirror and print its path
commit takes:
--put <path> <file>write
<file>at<path>;-reads standard input--put-exec <path> <file>the same, as an executable (mode 100755)
--delete <path>remove
<path>--edit <path> <old> <new>replace
<old>, which the file must hold exactly once, with<new>, in the file as the branch holds it, keeping its mode; edits to one path are made in turn. A change to part of a file need not send all of it--author <name> <email>the author; by default your handle and
<did>@knotless.invalid. The committer is the same as the author--base <commit>the commit the changes were made against; a path changed since then is a conflict
--batchas the owner, go through the repository’s gateway, which batches concurrent commits to the branch
--retries <n>how many times to replay onto a branch that moved (default 5); only a
fetch firstrefusal is replayed--jsonprint
{sha, parent, attempts, changed}instead of the commit’s hash; through a gatewayattemptsis 1
A path is relative, with no empty, . or .. segment, no NUL or newline,
and no .git segment in any case. A change that leaves the tree as it was
pushes nothing (changed: false). With --base, a path changed since then
fails conflict: <paths> changed since <base>. An edit whose <old> is not
in the file fails edit <n> of <path>: old is not in the file, and one it
holds more than once says how many times; an edit to a file not there, or
not a regular file, fails <path>: no file there to edit. When the branch
moved, edits are made again in the file as it is there.
Access
Section titled “Access”delegate <handle> <gateway did>let a gateway write for you, so collaborators can push through it
share <url> <handle> [--write]let an account read a private repository, or push too, printing
shared; with--writebefore you have delegated your writes to a gateway, delegates them to the appview’s first, asdelegatedoes, approved in the browser, printingdelegated to <did>before itunshare <url> <handle>take access away
members <url>who may read and push, a line each: the DID, then
owner,write(a collaborator),member(of the organization the owner is) orread, tab-separatedorg add <handle> [--admin] [--org <org>]invite an account to your organization, which your account is from then on (the first invitation says so), or change a member’s role, which keeps how their membership shows and when they joined;
--adminmakes them an admin, who may also change the organization’s members and make repositories as it. With--org, to an organization you are an admin of, through its gateway. Says where the invitation is kept: in the organization’s space, which only it and its members read, or, on a PDS without spaces, in its public records. An invitation lapses after 7 days; inviting again starts it over. Your organization acts through a gateway, so delegate first (knotless delegate)org remove <handle> [--org <org>]take a member out, or withdraw an invitation: they no longer push to its repositories, and leave the spaces of its private ones, but those shared with them on their own
org join <org> --public | --hiddenaccept an invitation, which makes you a member:
--public, anyone sees it, and you merge, close and label on all its repositories;--hidden, only its members see it, and on its public repositories you push and open pulls, but merging, closing, labelling and queueing take a public membership. Neither, and it says what each means. Again, to change which. Hidden takes a PDS with spacesorg leave <org>stop being a member, at once, or decline an invitation; warns if the organization’s gateway did not take you out of its spaces, and run again, asks it again
org members [<org>] [--public]an organization’s members, yours unless named, a line each: the handle (or DID), the role (
adminormember) and the standing (invited,lapsed,publicorhidden), tab-separated. The organization and its members see every one; anyone else, public members, and--publicshows them so to whoever asks. Or that it is not an organization, and, for yours, how to make it oneorg list [<handle>]the organizations an account is a public member of, a handle a line; yours, with no handle, hidden ones too
org delete --yestake every member out and withdraw every invitation: your account is no longer an organization. Without
--yes, says whom it would take outprotect <url> <branch> [--merges] [--queue] [--reviews <n>] [--code-owners] [--dismiss-stale]hold collaborators to moving the branch forward, never deleting it, and, without
--merges, adding no merge commits; with--queue, pulls land on it only through the merge queue (see queue-only branches); with--reviews <n>(0 to 10), a pull merges into it only once<n>of the owner and collaborators other than its author approve it, with--code-ownersonly once an owner of each path it changes does, and with--dismiss-staleonly approvals of its head as it is now count (see required reviews). It replaces any rule the branch hadunprotect <url> <branch>drop a branch’s rule
rules <url>the protected branches, a line each: the branch and
linearormerges, thenqueuewhen it takes pulls only through the merge queue, thenreviews <n>,code ownersanddismiss staleas it asks for reviews, tab-separated;protectandunprotectprint the same
Pulls and issues
Section titled “Pulls and issues”pull <url> <base> <head url> <branch> <title> [-m <body>] [--image <file>]... [--draft] [--reviewer <handle>]...open a pull to merge
<branch>of<head url>into<base>;-m -reads the body from stdin; each--imageuploads a picture and shows it after the body, by its file’s name, at the size its density says (collaboration.md);--draftopens it as a draft, which neither merges nor queues until marked ready; each--reviewer, by handle or DID, asks an account to review it, which it hears of once the pull is not a draft (drafts and requested reviewers). Asking yourself failsissue <url> <title> [-m <body>] [--image <file>]...open an issue
template <url> pull [<base>]/template <url> issuewhat the repository asks a pull’s or issue’s title and body to follow (templates)
list <url> [--pulls | --issues] [-q <query>]pulls and issues, newest first;
-qfilters them in GitHub’s search words (collaboration.md)show <url> <at-uri>a pull or issue and its comments, each followed by a
reactions: +1 2 heart 1line when anyone reacted, as the appview counts them, or for a private repository as its space has themcomment <url> <at-uri> <text> [--approve | --changes] [--image <file>]... [--line <path>:<n>[-<m>][:old] <text>]...comment, or review: a pull only, and not one you opened. Each
--linecomments on lines of the pull’s diff at its head,:oldfor lines it removes, starting a thread; sent with a<text>or verdict, as that review’s, otherwise on their own (<text>may then be''); at most 100. Given a comment’s URI, answers it in its threadresolve <url> <comment-uri>/unresolve <url> <comment-uri>resolve the thread a comment on a pull’s lines starts, or open it again: its author, the pull’s, the owner or a collaborator
edit <url> <at-uri> [--title <title>] [-m <body> [--image <file>]...] [--draft | --ready] [--reviewer <handle>]... [--no-reviewer <handle>]...change your pull, issue or comment;
-m ''drops a pull’s or issue’s body,-m -reads it from stdin; the pictures it keeps are those the new body shows. For a pull,--draftmakes it a draft and--readymarks it ready, asking its reviewers again as of now;--reviewerasks another account to review it and--no-reviewerstops asking one. Any of these on an issue or comment fails<at-uri> is not a pulldelete-comment <url> <at-uri>take back your comment or review
react <url> <at-uri> <content> [--undo]react to a pull, issue or comment with
+1,-1,laugh,hooray,confused,heart,rocketoreyes, printing the reaction’sat://URI; one already there is kept;--undotakes it back, printingtaken back; on a private repository the reaction is written in its space, as a comment isclose <url> <at-uri>/reopen <url> <at-uri>change an item’s state
label <url> <at-uri> <label>.../unlabel <url> <at-uri> <label>...as owner, collaborator or a triager of their scope, put labels on an item or take them off; prints those it carries now
labels <url>the labels a repository defines: name, color,
exclusiveand description, tab-separatedlabels <url> add <name> [--color <#hex>] [--description <text>] [--exclusive]/labels <url> remove <name>as owner, define a label (or redefine it), or take one away, off everything it is on
triagers <url> [add <handle> <scope>... | remove <handle>]the accounts, such as a triage bot, whose labels of those scopes count, below the owner’s and collaborators’; as owner, name one or stop naming it. Prints each as its DID and scopes, tab-separated. Named for a private repository, it is made a member of its space and invited, as a runner is, and
warning: <did> was not told of it: <why>follows when the invite could not be lefttriagers <url> eval <handle>how an account’s labels compare with everyone else’s, one line per scope the repository defines, in the order of its labels: the scope, the pulls and issues the account put a label of it on, how many of those someone else did too, and how many of those it agreed on (everyone else’s labels of the scope, worked out without the account’s, are its own), tab-separated
merge <url> <at-uri> [--keep-branch] [--wait <secs>] [--force]merge a pull;
--waitwaits for its checks, and--force, the owner’s alone, merges past them (see the merge gate). Into a branch protected with--queue, it queues the pull instead, unless forcedmerge <url> <at-uri> --queueput a pull in its branch’s merge queue, and return, printing
queued into <base>: <n> of <of>; takes no--keep-branch,--waitor--forcemerge <url> <at-uri> --checkmerge nothing: say whether it would merge now, how, and what you may do to it, as a pull’s page’s merge box shows it
revert <url> <at-uri> [-m <reason>]as owner or collaborator, undo a merged pull: one commit on a new branch
revert/<rkey>, cut from its target branch, and a pull of it into that branch, titledRevert "<title>"; prints the new pull’s URI. See reverting a pullqueue <url> [--json]the merge queues in order, and what was ejected
queue <url> --leave <at-uri>take a pull out of the merge queue
retarget <url> <at-uri> [--base <branch>] [--head <branch>]point a pull at other branches; the head only by the pull’s author
What they print:
pull,issuethe new record’s
at://URIcommentthe
at://URI of each comment it wrote, a line each: the review first, then its comments on linesresolve,unresolveresolved,unresolvedlista line per item:
<kind>,<state>,<uri>,<title>, tab-separatedshow<state> <kind> by <author>: <title>, thendraftfor a draft pull,review requested: <handles>for one that asks for reviews, thenclosed by <pull>for an issue a pull closed,mentioned in <pull>for each pull whose title or body names an issue, oldest first, ending, to close itwhere it names it after a closing keyword,reverts <pull>for a revert,reverted by <pull>for a merged pull whose revert merged, then for a pull each reviewer’s standing review asreview: <reviewer> <verdict> at <commit>(the first 12 characters of the head it looked at), then for an open pull in or ejected from the merge queuemerge queue: <state> at <createdAt>and where it stands, and for one ejected as checks failedlogs: <command>, theknotless checksthat reads their logs, then the body and each comment as<author> [<verdict>]: <body>, where a comment on lines sayson <path>:<lines>[:old] (<uri>[, resolved])before the colon, and an answer in a threadanswering <uri>mergethe commit the branch is now at, then
deleted <branch>and aclosed <issue>line per issue it closed; queued instead,<base> takes pulls through the merge queue: queued, <n> of <of>, then where to follow itmerge --queuequeued into <base>: <n> of <of>, then where to follow itmerge --check<state>,<method>andchecks <checks>, andreviews <passed or blocked>when its branch asks for reviews, tab-separated, then amissing <what>line for each thing its reviews lack (reviews 1 of 2,owners of web/: @alice or @bob,changes asked by @carol; owners who are the pull’s author alone add, its author alone, whose own approval never counts, so web/ needs another owner in CODEOWNERS), anuncounted <why>line for each approval that does not count (@dan's approval does not count: only the owner's and collaborators' approvals count), awarning <what>line for each CODEOWNERS line skipped, aconflict <path>line per file that conflicts,deletes <branch>for the branch it would delete,may <what>,queue onlywhen its branch takes pulls only through the merge queue, and last why it cannot merge, in the wordsmergewould refuse with, and that the caller may not merge it when it may notqueuea line per pull:
<position>,<base>,<state>,<uri>,<title>, tab-separated, where the state isqueued,pre-flight <workflows>while the pre-flights it waits for run, comma-separated, or<checking or landing> at <commit>; an ejected pull has-andejected, and a line after it with the reason;the merge queue is emptyif it isqueue --jsoneach pull waiting, with its
placeandbatch, or thepreflightworkflows it waits forqueue --leaveleft the merge queueedit,delete-comment,close,reopen,retargetedited,deleted,closed,open,retargeted
merge --queue refuses --keep-branch, --wait and --force (--queue takes none of --keep-branch, --wait and --force), and merge --check
those and --queue. Its state is clean, conflicting, blocked by
its checks or its reviews, or draft, whatever else holds, until its
author marks it ready (merge and merge --queue refuse one, in the
words its reason gives); its checks are passed, failed, running, pending (not run at
its head merged with its base as it is now) or none (the repository
names no runners, or has no workflow that runs on the pull, so nothing is
waited for, or it conflicts); its method
fastForward, merge or squash (into a branch protected as linear).
may lists, comma-separated, what you may do to it: merge and queue
(the owner and collaborators, the queue only with runners), force (the
owner), base and head as retarget changes them (those who may close
it, and its author), and rerun (the owner, with runners). queue takes one flag,
--leave or --json. show reads an item under a minute old from its
author’s repository when the appview has not indexed it yet. Where the
merge queue’s standing lines come from is in the merge
queue.
Checks
Section titled “Checks”agents <url> [add | remove <handle>]the accounts, such as a coding agent, whose pulls the runners check as they move, as code no one vouches for (see the runner); as owner, name one or stop naming it, printing
addedorremoved. Prints each as its DIDminutes <url>the minutes each runner the repository names counts for its owner’s public repositories in the last 30 days, a line each: the runner, then
<n> of <allowance> min in 30 days(addingpast it: runs go one at a timeonce past),<n> min in 30 days, no allowance, ornothing counted yet, and the soonest minutes to free up. Each runner counts on its own (see the runner)runners <url> [add | remove <handle>]the accounts whose checks of the repository readers show;
addmakes the account a member of a private repository, and invites it ascreatedoes (added, thenwarning: …if it could not be), whichremoveleaves it (unsharetakes it back). A runner added after secrets were set has none of them, as nothing keeps their values to seal again:warning: <did> gets the secrets set before only once each is set again: <names>.removedeletes the secrets sealed to itsecret <url> set <name> [--ref <pattern>]... [--runner <handle>]...as owner, give the repository’s runners the secret
<name>, read from stdin less one line ending, sealed to the key each publishes so only it reads it, and left at the appview (KNOTLESS_APPVIEW), replacing one of the same name;--ref, repeatable, limits it to pushes to those refs, as a trigger’srefsmatch;--runner, repeatable, by handle or DID, gives it to those of the repository’s runners alone, and takes back the copy any other of them had. Printssealed <name> to <runners>. Fails if the repository names no runners, a--runneris not one of them, or one it seals to publishes no key (a knotless-run from before).<name>is a shell variable’s, not startingKNOTLESS_RUN; the value at most 16 KiB, the refs at most 16. See Secretssecret <url> ls | rm <name>as owner, each secret given, one line per runner,
<name>\t<runner>\t<refs, or *>\t<updated>, never a value; or take one back from every runner, printingremoved <name>checks <url> [--ref <name>] [--commit <sha>] [--pull <at-uri>] [--queue <branch>] [--workflow <name>] [--log] [--images <dir>] [--wait <secs>] [--json]what those runners report, newest first;
--ref, of pushes to that ref and runs asked for there, in full or a branch or tag named bare, asmain(a pull’s are read with--pull);--queue, of the merge queue’s batches;--logadds the end of each log;--imagessaves each check’s pictures;--waitwaits for runs to finishrerun <url> --ref <name> | --pull <at-uri> [--workflow <name>]as owner, ask the runners to run it again, or only one of its workflows
rerun <url> --ref <name> --workflow <name> [-e <name>=<value>]...as owner, ask for one workflow there, with variables its steps see
workflows <url> [<ref>]the workflows at a branch or tag (the default branch), and what runs each there
checks prints a line per check: <status>, <workflow>, the pull’s URI
or the ref, the commit’s first 12 characters, startedAt and any
skipped note, tab-separated. --log follows it with the log, and
--images ends the list with an image line per picture saved, then a
tab and its path, named <workflow> <alt>.<ext> with / as -.
--queue takes a bare branch name. --commit takes a commit’s hex ID or
at least its first 4 digits, as git log --oneline prints them, in either
case; anything else fails with a commit is its hex ID, or at least its first 4 digits, not "<given>". --wait reads again every
KNOTLESS_POLL_MS until it has found a check and none is queued or running, or the
time is up. Up with none found, it fails with no checks of <what was asked> after <secs>s, such as commit 0838c57, ref refs/heads/main,
rather than print nothing and exit 0. Reads that fail in a way that may pass (a connection error, a
timeout, a 5xx or a 429) are retried with a backoff of up to 60 s. The MCP checks tool waits at most 300 s,
and returns the pictures with images: true. A repository naming no
runners is an error, not an empty list. A private repository’s checks,
logs and pictures are read from its runners’ repos in its space, with a
space credential.
rerun writes a dev.knotless.rerun record in the owner’s account and
prints its AT-URI. A bare branch or tag is taken as its full ref, and a ref
the repository lacks is refused (<url> has no ref <name>), as is a
repository naming no runners. It asks for one ref or one pull (name one ref or one pull to run again); -e needs --workflow (variables go to one workflow: name it) and a ref (variables go to a workflow run at a ref, not a pull's). At most 32
variables: a name is 1 to 64 letters, digits or _, not starting with a
digit or with KNOTLESS_RUN, and a value at most 4096 bytes.
workflows prints the ref in full, then a line per workflow in file name
order: its name, then what runs it there, comma-separated, of push,
pull (a pull into it) and asked (rerun --ref --workflow may ask for
it: it runs on a push there, or its asked trigger names the ref), then,
for one that does not load, why. A ref it lacks is refused (<url> has no ref <name>).
Releases
Section titled “Releases”publish <url> <package> <version> <file>... [--commit <sha>] [--check <at-uri>] [--notes <text>] [--oci <dir>]as owner, publish a version of a package: its files, uploaded as blobs, then its
dev.knotless.releaserecord, never changed after (see releases). Prints the record’s AT-URI.--notes -reads the notes from stdinreleases <url> [<package>]the versions published, by package and then highest version first
download <url> <package> [<version>] [--file <name>]... [-o <dir>]a version’s files, or the latest’s, each checked against its size and SHA-256, into a directory (
.)
publish names each file by its path’s last part. A package’s name
follows npm’s rules: lowercase letters, digits, -, ., _ and ~, not
first a . or _, after an optional @scope/, at most 214 bytes. A
version is Semantic Versioning 2.0, such as 1.4.0 or 2.0.0-rc.1. A
release holds 1 to 64 files, by distinct names of letters, digits, .,
_, + and -, not first a ., and none empty. Each is split into
blobs of KNOTLESS_CHUNK_SIZE as a large pack is. In a runner’s step,
--commit is KNOTLESS_RUN_SHA unless given. A version published before
is refused before anything is uploaded, and by the record’s create if
another publish got there first: <package> <version> is published already: <at-uri>. A private repository’s are refused: a private repository's releases are not published yet.
--oci <dir> publishes an image, for knotless-serve’s registry at /v2/
(api.md): the files of the OCI image layout in dir,
as docker save or docker buildx build --output type=oci,tar=false
leaves one, beside any named. They are its index.json, which must name
one manifest (index.json names more or less than one manifest), and
each blob under blobs/sha256 as sha256-<hex>, checked against its
name (blobs/sha256/<hex> is not the blob its name says); the manifest
the index names must be among them (index.json names <digest>, which is not among the blobs). As a release holds at most 64 files, an image
holds at most 63 blobs, its manifests, configs and layers together.
releases prints a line per version: the package, the version,
createdAt, the commit’s first 12 characters (or -) and the files’
names, comma-separated, tab-separated.
download takes the package’s latest, its highest version without a
pre-release part, unless a version is named, and every file unless
--file names some. It prints a line per file: the package, the
version and the path written, tab-separated. A file whose bytes do not
match fails with <name>: what was read is not the file its release names; a version or file the release lacks with <url> has published no <package> <version> (latest for none) or <package> <version> has no file <name>.
Shared steps
Section titled “Shared steps”uses update [-C <dir>] [<uses>...]pin the shared steps the checkout’s workflows use, and those they use, in
.knotless/uses.lock(see shared steps): each to the highest release its@takes; with<uses>named, only those, the rest as the lock had them
A shared step is released as the package @step/<step> with publish,
its one file the step file and --commit the commit it is at. uses update reads the workflows under .knotless/workflows in dir (.),
and takes for each uses the highest version that its @ takes, of
those without a pre-release part unless the @ has one. It prints a
line per uses pinned: the uses, the version it was pinned at (or
-), the version now and the commit’s first 12 characters,
tab-separated. It fails, writing nothing, with <dir> has no .knotless/workflows; <url> has released no @step/<step> that @<spec> takes; @step/<step> <version> names no commit it was made from;
@step/<step> <version> holds no <step>.yml; @step/<step> <version>'s <step>.yml is not .knotless/steps/<step>.yml at <commit>, when the file
released and the file at its commit differ; or when the step file
cannot be read at that commit.
The merge gate
Section titled “The merge gate”In a repository naming runners, merge refuses a pull until each
workflow that runs on it has passed at its head merged with its base as
it is now (design). It says which:
<pull> did not pass its checks at its head, <h12>, merged with <branch> at <b12>: <workflow> (<status>), ...... are still running: ..., each still waiting for a job on its runner named<workflow> (queued), with`--wait <secs>` waits for them<pull> has no checks at ... of <workflows>, then why: none has run yet, the newest are of an older head, or the newest are of the branch at another commit, andIt is not checked yet
Each ends Their logs: `knotless checks <url> --pull <pull> --log`; the owner merges past them with `--force` . --force from anyone else is
refused: only the owner of <url> merges a pull past its checks. Over
MCP, merge takes wait (at most 300 s) and force.
Moderation and operations
Section titled “Moderation and operations”hide <url> <at-uri>/unhide <url> <at-uri>as owner, hide a pull, issue or comment
block <handle> [<url>]/unblock <handle> [<url>]leave out what an account writes about your repositories, or about one, printing
blocked <did>orunblocked <did>; yourself failsthat is you, another’s repositoryonly <url>'s owner can keep accounts out of itblocksthe accounts you block, the latest first: handle,
all your repositoriesor the repository’sknotless://URL, and when, tab-separated. Blocks naming a private repository live in its space, and are not listedreport <handle, url or at-uri> <reason> [-m <text>]report to the moderation service; the reason is
spam,violation,misleading,sexual,rudeorother; printsreport <id> sentadmin takedown <handle, url or at-uri> [--undo]as the appview’s operator, take a subject down, or restore it; prints
taken downorrestoredadmin status <handle, url or at-uri>as the operator, a subject’s moderation status:
taken downorupadmin reports [<limit>]as the operator, the reports received, newest first (default 50): a line per report of
<id>,<reporter did>,<reason>,<subject>,<text>, tab-separatedadmin stats [--days <n>] [--json]as the operator, how the appview is used, day by day, from
dev.knotless.getUsage(14 days by default)admin logs [--trace <id>] [--message <m>] [--level <l>] [--min-ms <n>] [--limit <n>]as the operator, the lines the appview logged, from
dev.knotless.getLogs, one JSON line each, oldest first: a request’s by its trace (up to 500, unless--limitsays fewer), or the last (100 by default, at most 500) of a message, a level and above, or a time; one of them is neededstatus [<server>] [--repo <handle>/<name>] [--json]a
knotless-serve’s health, from/_status: its feeds, index, a mirror and its disk’s room; the server defaults toKNOTLESS_APPVIEW
A report’s reason is sent as com.atproto.moderation.defs#reason<Name>,
as reasonSpam. Its subject is a com.atproto.admin.defs#repoRef for a
handle or DID, and a com.atproto.repo.strongRef with the record’s
current CID for an at:// URI, or for a knotless:// URL’s repo
record. It goes to the #atproto_labeler service of KNOTLESS_MODERATOR’s
DID document, else of the appview’s /.well-known/did.json, with a service
token from your PDS; with neither set it is an error. The admin commands
need an appview (KNOTLESS_APPVIEW) and an account it names in
KNOTLESS_ADMIN_DIDS: each call carries a service token from your PDS
for its method, made out to the DID in the appview’s
/.well-known/did.json.
status fails with <server> is unhealthy when /_status says it is
not, and with status takes the server's URL: KNOTLESS_APPVIEW is set empty when it
has neither.
knotless-serve
Section titled “knotless-serve”--listen <addr>where to listen
--ttl <secs>how old a mirror may be before a read syncs it, when no feed is up; whole seconds
--jetstream <wss://host>a Jetstream v2 host to follow
--firehose <wss://relay or pds>a firehose to follow instead
--index <file>keep an appview index in this SQLite file
--backfill <https://pds or relay>at start, index every account on a PDS, or those holding knotless records on a relay
--relay <https://relay>backfill from this relay when the index was rebuilt (a new file, or another build’s tables), and every ten minutes after
--public-url <https://host>the URL the server is reached at; makes it a gateway, as
did:webof the host, and serves MCP at/mcp--labeler <did>honour this labeler’s takedowns; repeatable
--default-runner <did>the runner a new public repository names unless its owner names others, as
dev.knotless.describeServeranswers; one only, as each runner a repository names runs all its checks (--default-runner names one runner); empty, none; anything else not a DID is refused (--default-runner takes a DID, not <value>)--client-minimum <version>the oldest client version this appview asks for, as
dev.knotless.describeServeranswers: older clients refuse to run until upgraded. For a release found to harm repositories; anything not a version is refused; empty, none--client-minimum-reason <text>why, which those clients print with their refusal
--web <http://app>the SvelteKit app that draws the pages
--releases <dir>client builds to hand out under
/releases, with/install.sh--stale <secs>how long a feed may be silent before
/_statuscalls it dead; whole seconds--usage <file>count requests and page views in this SQLite file, for
knotless admin stats--logs <file>keep each line logged in this SQLite file too, for
knotless admin logs: every serve writing it, across deploys, for 30 days--logs-keep <lines>how many lines at info and below
--logskeeps, the last of them; those above stay their 30 days--check-index <file>only check that the index there is of this build’s tables, so starting would keep it
--metrics <addr>answer
/metrics(Prometheus) andPUT /log(the log’s level) there, apart from the rest; printsmetrics on <url>afterlistening on--sign-up-pds <https://pds>the PDS where the pages send someone making an account (Logging in)
--oauth-url <https://host>where the PDS sends the gateway’s OAuth logins back to, when that is not
--public-url: an https front for a server at a loopback address, as the scenarios run it--bots <dir>let the accounts
KNOTLESS_BOT_DIDSnames log in at/oauth/bot, keeping their sessions in this config directory forknotless-runandknotless-triage(Bots)
An empty --jetstream, --firehose, --backfill or --default-runner is the same as none,
so a setting such as --firehose "$KNOTLESS_FIREHOSE" left unset turns it
off. --labeler takes --index. --private is gone, and refused
(--private is gone: knotless-serve shows a private repository to each member who logs in to its pages (--public-url), read as them, …): a
server people log in to shows each member their private repositories.
--bots takes --public-url (--bots takes --public-url, where bots log in), and
a --public-url is http(s)://host[:port] with no path.
An unknown flag prints unexpected <flag> and the usage, and a flag
without its value <flag> takes a value; --ttl and --stale that are
not whole seconds print --ttl takes seconds or --stale takes seconds.
These exit 2. An error once started prints knotless-serve: <error> and
exits 1. Once bound, it prints listening on http://<addr>, and it stops
cleanly on SIGTERM or SIGINT.
--check-index <file> prints <file>: this build keeps it, and passes a
file that does not exist. It fails with <file>: its tables are version <v>, this build's <version>: starting would rebuild it otherwise.
/install.sh, served with --releases, installs knotless,
git-remote-knotless and knotless-mcp for aarch64 or x86_64, macOS
or Linux (musl), with curl or wget. It checks each download’s SHA-256, and
moves each binary into place, so a running copy is replaced. The routes
are in the API reference.
knotless-run
Section titled “knotless-run”knotless-run [flags] <handle>/<name>... watches the repositories
named, and with --discover those whose records name its account as a
runner (then it needs none named), acting as KNOTLESS_IDENTIFIER with KNOTLESS_PASSWORD, or as
the account logged in with knotless login. A private repository takes
an OAuth login, for a space credential, and the runner’s account a
member of it: knotless create --private and knotless runners <url> add make it one, and invite it, which --discover reads.
--executor docker | shellrun steps in a container, or as the runner’s user (the library’s
Options::newdefaults toshell)--image <image>the image for a workflow that names none
--jobs <n>runs at once, over every repository
--pull-jobs <n>of those, how many runs other than pushes to a default branch and the batch landing next may take; at most
--jobs--owner-jobs <n>of those, how many the runs of one owner’s repositories may take; at most
--jobs--allowance <minutes>the minutes of steps each owner’s public repositories’ runs may take in 30 days, the runner’s own account’s aside; past it, that owner’s runs go one at a time
--private-allowance <minutes>the same for private repositories’ runs, counted apart
--unmetered <did>an owner whose runs, like the runner’s own account’s, count against neither allowance, and may take every job, past
--owner-jobs; repeatable--step-timeout <secs>the longest a step may run, and a container may take to start
--run-timeout <secs>the longest a run’s steps may take together; a step going when it is up is killed and the rest skipped. A run asked for by name is not held to it
--cpus <n>under
docker, the CPUs a run’s container may use, as docker’s--cpustakes them (2,1.5)--memory <GiB>under
docker, a whole number of GiB a run’s container may use, with no swap beyond it; past it the kernel kills what is running and the step fails--pids <n>under
docker, the processes and threads a run’s container may have at once--network <name>under
docker, the network runs join; made if missing as a bridge whose containers cannot reach each other, used as it is if there already--runtime <name>under
docker, the runtime runs’ containers start with, as docker’s--runtimetakes one:runscruns them under gVisor, a kernel of its own between a run and the host’s. The daemon must know it (see the runner host)--trusted-runtime <name>under
docker, the runtime of a run whose code an owner--trustnames vouches for: a push to their repository, or a pull or batch every writer of which they vouch for.runcspares it gVisor’s cost--trust <did>an owner whose vouched-for runs start under
--trusted-runtime; repeatable--resolv <file>under
docker, a file on the docker host that runs’ containers see as their/etc/resolv.conf. Underrunscon a network docker made, a run resolves no names without one: docker’s resolver there is reached through NAT rules gVisor does not apply--paused <file>a JSON object of owner DIDs, or
*for all but the runner’s own account, each to why their runs are paused; read again on every pass, so editing it takes effect without a restart. A paused owner’s runs are filed aserror:paused by this runner's operator: <why>--keep-checks <days>how long the default branch’s checks are kept, each workflow’s newest whatever its age; the runner prunes its checks of no more use each hour. 0 keeps every check
--sweep <secs>how often every repository is read again, in case an event was missed, and leftover containers removed
--stop-wait <secs>how long a stop waits for runs going before it cancels them; 0 waits not at all
--poll <secs>how often a private repository’s space is asked whether anything was written there, which Jetstream never carries; while
KNOTLESS_APPVIEW’s appview passes on its writes, no more often than once a minute (see Private repositories)--cache-max <GiB>a whole number of GiB: the size at which a cache’s new base is not used, to start afresh, and the most kept layers may take in all
--disk-floor <GiB>a whole number of GiB kept free on the disk
--cachesis on: below it, a run about to start first sheds every cache’s base no run reads and every kept run’s layers, then waits for room, looking every 30s; 0 keeps no floor (see Room on the disk)--caches <dir>where caches’ bases and runs’ layers are kept; a path the docker daemon sees under the same name
--secrets <file>the secrets file
--secrets-key <file>where the runner keeps the key owners seal secrets to, made if missing, and published as its
dev.knotless.runnerKeyrecord (see The runner’s key)--state <file>the state file
--work <dir>where checkouts go while their steps run; made if missing
--jetstream <wss://host>where events come from
--discoveralso watch every public repository whose record names this runner’s account in
runners, as the appview’slistRepos?runner=lists them and Jetstream brings them; one no longer naming it is left. NeedsKNOTLESS_APPVIEW(see Repositories that name it)--pullsalso run open pulls, at their head merged with their base, without secrets; takes
docker--pulls-only <handle>/<name>watch that repository for its open pulls alone, never its pushes or its merge queue, which another runner runs; as there, a queued pull does not run on its own, and the rest run again as their head moves, not their base; repeatable; takes
--pulls--queueland the repositories’ merge queues where its account may push, or where they name it and no runner that may (Who lands a queue); takes
docker--batch <n>with
--queue, the most pulls in a batch--depth <n>with
--queue, 2 checks the next batch on top of the one being checked; 1 turns that off; nothing else--notice <secs>with
--queue, how often the owner’s and collaborators’ queue records are read
<cache dir> is KNOTLESS_CACHE_DIR’s. --jobs, --pull-jobs, --owner-jobs, --allowance, --private-allowance,
--sweep, --batch, --notice, --poll, --run-timeout, --memory
and --pids take at least 1, as in --batch takes a number of pulls, at least 1 and --notice takes a number of seconds, at least 1. --depth 0 is refused with --depth takes a number of batches, at least 1, and more than 2 with --depth is 1 or 2: one batch at most runs ahead. --pulls and
--queue refuse --executor shell (--pulls and --queue run code anyone can open a pull with: they take --executor docker), and so do --cpus,
--memory, --pids, --network, --runtime, --trusted-runtime and
--resolv (--cpus, --memory, --pids, --network, --runtime, --trusted-runtime and --resolv hold a run's container: they take --executor docker).
--cpus takes a number above 0 (--cpus takes a number of CPUs, more than 0, not <value>). --pulls refuses
to start with KNOTLESS_APPVIEW set empty; --queue starts so, but
its reads of pulls then fail. A repository that cannot be found stops the
start: <label>: no repository by that name this runner can read.
A usage error exits 2 and prints the usage; an error at run time exits 1,
as knotless-run: <error>. Once started, the runner logs knotless-run: watching <repositories> as <did> to standard error, and an error in one
repository as knotless-run: <label>: <error>, and goes on: the same
error again only once a minute, with how many times it was seen. Each pass
logs how long it took, and one going 5 minutes says where it is, every 5
minutes (What a pass logs). A pass that
finds a repository’s key changed fails with <label> no longer names the repository it did.
It follows Jetstream’s subscribeEvents for the dev.knotless.*
collections, with commit, identity, account and sync events, over
the xrpc.v1.json websocket subprotocol, without a cursor. After a
session ends it connects again in 500 ms, doubling the wait after each
failed attempt up to 60 s, and back to 500 ms once connected. A
connection silent for 60 s gets a ping, and silent another 60 s is given
up.
What the runner keeps on disk, its containers and its volumes are in knotless-run internals.
knotless-triage
Section titled “knotless-triage”knotless-triage (--decide <url> --model <name> | --classify <command>) --scopes <scope>,... [flags] [<handle>/<name>...] triages the repositories
named, and with --discover those naming it, acting as
KNOTLESS_IDENTIFIER with KNOTLESS_PASSWORD, and reads public
repositories’ issues through KNOTLESS_APPVIEW.
--decide <url>a decision model’s endpoint: one taking
{model, state, questions}with a bearer token, as TypeSafe’s Jev (https://api.typesafe.ai/v1/systemone) or Cloudflare’s Clef on Workers AI (https://api.cloudflare.com/client/v4/accounts/<account>/ai/run/@cf/cloudflare/clef) do. Its token isKNOTLESS_DECISION_TOKEN--model <name>with
--decide, the model asked, asjev-latest,cleforclef-flash--threshold <p>with
--decide, the least probability, from 0 to 1, that sets a label--classify <command>instead of
--decide, a command run withsh -conce per issue to judge--scopes <scope>,...the scopes it chooses labels of, as
kind,area--jetstream <wss://host>where events come from
--sweep <secs>how often every repository is read, whatever events say
--settle <secs>how long after an event the repository is read, for the appview to index the issue
--timeout <secs>how long the classifier has for an issue before it is killed
--onceread each repository once, and stop
--discoveralso triage every repository whose record names this account in
triagers, in the scopes it names it a triager of that--scopesholds: those the appview’slistRepos?triager=lists, private ones that invited it, and those Jetstream brings, at the start and every--sweep. One no longer naming it, or that cannot be read, is left, and found again if it still names it. NeedsKNOTLESS_APPVIEW
A decision model is asked one question per scope whose labels are all
exclusive, a choice among them named by their names after the scope,
with none_of_these for none, and, of any other scope, one noul
question per label, named by the label with _ for each character other
than a letter or digit. Each label’s description, when it has one, goes
with it. The state is the issue’s title and body. A label is set when
its probability (a choice’s confidence, a noul’s noul) is at least
--threshold. The record’s judged gives the model the answer names,
or --model, and as its reason each question’s answer and probability,
as kind/bug 0.91; area/web 0.10, or kind: none 0.80. An answer that is
not a success, or has no answers (at the top, or in result as Workers
AI gives them), is logged as judging failed.
A command reads one issue as JSON on standard input: subject (its
AT-URI), author (a DID), title, body when it has one, and labels,
the repository’s definitions of the labels to choose among. It writes
{"labels": [...], "model": "...", "reason": "..."} on standard output;
model and reason may be left out, and are kept to 200 and 500
characters. A name not offered is passed over, and of a scope’s exclusive
labels only the first offered is set. A classifier that exits non-zero,
answers something else, or a classifier of either kind that takes longer
than --timeout, is logged as judging failed,
and the issue is asked about again at the next read. Each issue judged is
logged as judged, with the labels set.
A usage error exits 2 and prints the usage, as name its scopes and a repository, or --discover or name one classifier, --decide or --classify; an error at run time exits 1, as
knotless-triage: <error>, as --discover finds repositories through an appview, and KNOTLESS_APPVIEW is set empty. With --discover it logs each
repository it finds as found and each it leaves as left; a listing or
the invites it could not read, as finding repositories failed. Watching, it stops at the start if a
repository cannot be opened; afterwards a repository that cannot be read
is logged as reading the repository failed, and it goes on. With
--once, any such error stops it.
It follows Jetstream as knotless-run does, reconnecting in 500 ms, doubling
up to 60 s, and giving a connection up once it stays silent 60 s past a
ping. It reads every repository on connecting; an issue written about
a watched repository, or a watched owner’s repository record, has it read
the repositories again after --settle; with --discover, any repository
record written has it read that repository’s, to find one that names it.
knotless-agent
Section titled “knotless-agent”knotless-agent review --command <command> [flags] [<handle>/<name>...]
reviews the open pulls of the repositories named, and of every repository
an --owner holds, that ask its account for a review, acting as KNOTLESS_IDENTIFIER with KNOTLESS_PASSWORD, and
reads public repositories’ pulls through KNOTLESS_APPVIEW. Reviews by
Claude sets one up, and review
agents says why it works as it
does.
--command <command>the command run with
sh -conce per review, asknotless-claude-review--allreview every open pull that is not a draft, asked or not
--rounds <n>the most reviews of one pull, one at each head it is pushed to
--timeout <secs>how long the command has for a pull before it is killed
--jetstream <wss://host>where events come from
--sweep <secs>how often every repository is read, whatever events say
--settle <secs>how long after an event the repositories are read, for the appview to index the pull
--onceread each repository once, and stop
--owner <handle|did>also watch every public repository this account holds, listed from its PDS at each read, so one it makes later is watched too. Repeat it for more accounts. An owner whose listing fails is logged as
listing an owner's repositories failed, and its last listing stands
It reviews a pull that is open and not a draft, that its owner, a
collaborator or a member of the owner’s organization opened, and whose
reviewers names its account (or any, with --all), once at each head
commit, up to --rounds times. It never reviews its own. A head whose
changes against the base are those of the head it last reviewed, as after
a merge of the base into the branch, is not read again: its last verdict
is written at the new head, beginning Carried forward:, and counts as no
round.
The command reads one pull as JSON on standard input: repo
(knotless://<owner>/<name>), pull (its AT-URI), title, body,
author (a handle, or a DID where none resolves), base (the branch it
merges into), head (repo, branch and the commit it is at), round
(from 1), rounds, earlier, its own earlier reviews of the pull,
each verdict, body and commit, and instructions, what
.knotless/review.md says on the pull’s base branch, when it is there. It writes {"verdict": "approve" | "changes", "body": "...", "lines": [...]} on standard output, each of
lines {"line": {"path", "line", "startLine", "side"}, "body"} as
comment --line takes them (at most 100); lines may be left out. Its
standard error goes to the agent’s. A command that exits non-zero,
answers something else, or takes longer than --timeout, is logged as
reviewing failed, and the pull is read again at the next read. A review
written is logged as reviewed, with its verdict. One whose head moves as
the command reads it is read again at the new head, and nothing is
written of the first answer.
knotless-claude-review (crates/knotless-agent/claude-review, in the
server image at /usr/local/bin) is such a command: Claude Code, headless,
reading the pull through knotless-mcp --read-only, logged in as the
agent, and following instructions and KNOTLESS_REVIEW_PROMPT besides
its own rules. It takes CLAUDE_CODE_OAUTH_TOKEN (from claude setup-token) or ANTHROPIC_API_KEY, KNOTLESS_REVIEW_MODEL for
Claude’s --model,
KNOTLESS_MCP for the knotless-mcp to run and CLAUDE for the claude.
A usage error exits 2 and prints the usage, as name its command and a repository, or --owner; an error at run time exits 1, as knotless-agent: <error>.
Watching, it stops at the start if a repository cannot be opened;
afterwards one that cannot be read is logged as reading the repository failed, and it goes on. With --once, any such error stops it.
It follows Jetstream as knotless-triage does. It reads every repository
on connecting; a pull written about a watched repository or any of an
owner’s, a repository record an owner writes, or a ref pushed by an
account whose pull asks it, has it read the repositories again after
--settle.
Environment
Section titled “Environment”Identity and login
Section titled “Identity and login”KNOTLESS_IDENTIFIERthe account to act as
KNOTLESS_PASSWORDan app password, instead of OAuth:
KNOTLESS_IDENTIFIER’s, used only to log in as that account, and only when it has no stored session that still refreshesKNOTLESS_BROWSERthe command an OAuth login opens its page with
KNOTLESS_SERVICEthe PDS every identity is served by, skipping DID resolution
KNOTLESS_PLC_URLthe PLC directory
KNOTLESS_HANDLE_RESOLVERwhere handles are resolved, in order:
well-known:<scheme>for/.well-known/atproto-did, or a service URL for XRPCKNOTLESS_DID_WEB_SCHEMEthe scheme
did:webresolves overKNOTLESS_IDENTITY_TTL_SECShow long a resolved identity is cached
KNOTLESS_CA_CERTSa PEM bundle of extra CAs to trust, such as a local network’s own
Servers
Section titled “Servers”KNOTLESS_APPVIEWthe
knotless-serveindex that lists pulls and issues on public repositories; set empty, there is none.knotless-serve’s/mcpreads its own index before the defaultKNOTLESS_MODERATORthe DID of the moderation service reports go to, by its
#atproto_labelerserviceKNOTLESS_ADMIN_DIDSon
knotless-serve, the accounts that operate it, comma-separated: the admin methods take their service tokens, for theadmincommandsKNOTLESS_BOT_DIDSon
knotless-servewith--bots, the accounts that may log in at/oauth/bot, comma-separatedRUST_LOGwhat
knotless-serveandknotless-runlog, as JSON lines on stderr:debug, orinfo,knotless::outgoing=debugfor each call made,knotless::git=debugfor each git commandKNOTLESS_SLOW_MSon
knotless-serveandknotless-run, how slow a request served or made, or a git command, is before it is logged at warn, and onknotless-servea tenth of it, an index’s SQL statement; onknotless-run, a pass taking this long logspass endedat info rather than debug
Storage
Section titled “Storage”KNOTLESS_INLINE_MAXthe largest pack kept inline in its record
KNOTLESS_CHUNK_SIZEthe size of a large pack’s, or a release file’s, blob parts
KNOTLESS_COMPACTwhen to compact:
background(a detachedknotless compact),inline(before the push returns) oroffKNOTLESS_COMPACT_THRESHOLDthe packs, counting the push’s own, at which the owner’s
git pushstarts a compactionKNOTLESS_COMPACT_GRACE_SECShow old a pack must be to be compacted away
KNOTLESS_CLAIM_GRACE_SECShow old a claim must be to be dropped
Client
Section titled “Client”KNOTLESS_TIMEOUT_SECSa request’s timeout; connecting times out after 10 s, and an idle connection is dropped after 4 s. A handle’s
/.well-known/atproto-didand adid:webdocument get 3 s, as atproto’s resolvers doKNOTLESS_POLL_MShow often
--wait, onchecksandmerge, reads checks againKNOTLESS_CONFIG_DIRwhere sessions are kept
KNOTLESS_CACHE_DIRwhere mirrors and caches are kept
KNOTLESS_NO_UPDATE_CHECKset, the appview is not asked for its newest release or the oldest version it asks for
KNOTLESS_IGNORE_MINIMUMset, run although older than the appview asks for
KNOTLESS_INSTALL_DIRwhere
knotless upgradeinstalls
A number that does not parse is its default. Every process shares one HTTP client, so requests reuse connections.
The config directory holds sessions/<did>.json (mode 0600), me (the
last account logged in), credentials/ (space credentials) and, on a
gateway, oauth-client-key. The cache directory holds:
| File | |
|---|---|
identity.json | handle or DID to DID and PDS, for KNOTLESS_IDENTITY_TTL_SECS |
handles.json | DID to verified handle, as long |
servers.json | each PDS’s blob limit, as long |
keys.json | each repository’s key, and whether it was found public or in a space; never expires |
emails.json | the accounts whose first-push email claim was already looked up here |
update.json | the appview’s newest release and the oldest version it asks for, and when it was asked, for a day |
mirrors/ | the bare mirrors and their lock files |
tmp/ | scratch repositories and indexes, removed when done |
Each JSON file is written whole and renamed into place. A missing or unreadable one reads as empty.
Install script
Section titled “Install script”| Variable | Default | |
|---|---|---|
KNOTLESS_INSTALL_DIR | $HOME/.local/bin | where the binaries go |
KNOTLESS_VERSION | the latest | which release |