What this project has had to understand about Fossil, beyond its manual: the points that changed a design or caused a failure. It is not a guide to Fossil.
Serving
- Only the server writes the served repository. Every other process
reaches it through HTTPS sync with its own login (decision 3 in
HISTORY.md). - An extension page's output is held until the program exits. A CGI
program under
--extrootcannot stream: three events written two seconds apart arrived together at six seconds (Fossil 2.27, measured for f718b75092). A live stream needs its own server. - Run as root, the server jails itself into the repository's directory,
so an
--extrootoutside it is not found. The served host runs with--nojail(scripts/hosts.json). email-urlis the repository's public address.projectwrites it as the server of the new repository's project password file, and a run's clone is made from that file's server URL. A clone's copy of a setting is not the server's: reading it from a clone gave a stale host name (bafb3a31bc).
Hooks
- The after-receive hook is started inside a write transaction, from the
backoffice (Fossil's hook documentation).
As Fossil 2.27's
source shows (
hook_backofficein hook.c,popen2andpclose2in popen.c,backoffice_run_if_neededin backoffice.c):- Standard input is the list of artifacts received since
hook-last-rcvid, one per line, a web post included: each its hash, a space and Fossil's summary of it (describe_artifactsin name.c), which begins with its kind:check-in to BRANCH by USER on DATE,forum-post ROOTorforum-reply-to ROOT(the first 14 hex digits of the thread's root post),ticket TICKET(the first 21 of its ticket-id),file NAME,tag …,wiki "NAME",event …andattachment …; anything elseunknown, or as an artifact others name it, such ascheck-in referenced by "NAME" tagfor one not received. - Standard output is a pipe Fossil closes without reading; SIGPIPE is ignored, so a write there fails.
- Standard error, under the server, is
/dev/null: the backoffice child callssetsid(), reopens descriptors 0 to 2 on/dev/nulland closes 3 to 99 before running hooks. The exit code is ignored. Fossil keeps nothing a hook says; a hook that needs a log writes its own. - Fossil does not wait for the hook:
pclose2only reaps children that have ended. The backoffice writes the list, recordshook-last-rcvidand commits while the hook goes on, in the backoffice's own session. So a hook is already detached; one that reads the repository must wait out that write transaction (a busy timeout). - A web post reaches the hook like a sync, and Fossil records its
receipt (
rcvfrom) under the signed-in user (98fc903663).
- Standard input is the list of artifacts received since
- The backoffice starts only after a page that answers 200 (
cgi_replyin cgi.c). A form's POST answers 302, so a web post reaches the hook with the page shown after it. Each start takes a lease for a minute: a page within it queues a backoffice that waits for the lease to end, and withbackoffice-nodelayon gives up, leaving the receipt to a later page;backoffice-disablestarts none, so hooks run only underfossil backoffice. Tried with Fossil 2.27 on a scratch server, 2026-10-06: a post made a second after a page reached the hook 61 seconds later. - Run as root without
--nojail, the server's backoffice ran no hook: the server jails itself into the repository's directory, and a page's backoffice child left no lease and ran nothing (Fossil 2.27, the same scratch server). The served host runs--nojail, and so does the hook spec's fixture. A backoffice leaves its server's process group (setsid), so one waiting out a lease outlives its server by up to a minute. - Hooks are a JSON list in the
hookssetting,{"cmd", "type", "seq"}, named by their place in it:fossil hook deleteremoves the places given one at a time, each moving those after it, so the last goes first.%Rbecomes the repository's path,%Fits executable, unquoted. fossil settings NAMEshows the value Fossil reads: the repository's (local), or else the account's global one (global).- OCX installs only Fossil's export:
%F git export --if-mirrored -R %R. No hook schedules work; the daemon reads complete pulls (How a post reaches an agent). - A pull runs no hook: on a clone,
fossil pullbrought a ticket change and ran no after-receive hook;fossil backofficethen ran it, listing the change (Fossil 2.27, a scratch server, 2026-10-12).
Checkouts and reads
Tried with Fossil 2.27, 2026-10-07:
- A checkout is its checkout file,
.fslckout(or the older_FOSSIL_), an SQLite database whosevvartable holds therepositoryit opens and itscheckout-hash; Fossil finds it from the working directory up, and OCX reads it so too (theuse-fslckoutbranch's example, 5909a744f5). - A checkout's
repositorymay be relative, read from the checkout's root (db_repository_filename);fossil openkeeps the path as given only when run in the checkout with no--workdir, which makes it full. OCX opens each checkout so, by--chdir, so a sandbox showing a run's workspace at another path still finds the clone (decision 86, tried 2026-10-19). fossil infooutside a checkout exits 0 with norepository:line; in a checkout whose repository is gone it exits 1.fossil whatisof a name that names nothing exits 0, sayingunknown:; outside any repository it exits 1.fossil sqlprints a NULL asNULL, unquoted, so a read that may find none coalesces it to nothing.fossil remoteprints the URL with its login but not its password.- What a commit would take:
fossil changes --hash --differlists both edits and files Fossil does not track;--extranarrows it to the untracked alone, so an edit went unlisted (trunk's workspace gc). A stash hides an edit fromchangesand shows infossil stash list, each stash's line beginning with its number. A clone'sunsenttable counts the artifacts it has not pushed: 0 fresh, 2 after a commit of one new file, 0 after the push (tried 2026-10-19, for HISTORY.md decision 80).
Clones and logins
A tag pushed by a login without Check-In (
i) is refused: the push printsError: not authorized to write, sends nothing, and still endsPush donewith status 0, as a sync endsSync done(Fossil 2.27, tried on a scratch server, 2026-10-06).ocx2 auditreads that line, and from a clone reads its verdict back by a second push that must send nothing.Autosync pulls before each commit and pushes after it; with the server away, the commit is refused. Workspace clones of an HTTPS remote run with it on (0160971667).
A clone of a repository file copies every login's password hash, so OCX never makes one: it clones by URL.
A remembered remote URL holds the password, obscured, not encrypted:
fossil test-obscureturns the stored value back into the password (tried with Fossil 2.27 on a scratch repository, 2026-10-03). Whoever reads a clone holds its login, so each holder gets a login of its own.fossil clone --save-http-password URLremembers the password the URL carries; without it, and with no terminal to ask at, the clone forgets it and its first pull is refused, andfossil remote URLdoes not save it either (Fossil 2.27, 2026-10-06).fossil --args -reads more arguments from standard input, one per line, where they stand among the others. A URL holding a password given that way never appears in the process's command line, which any account on the host may read in /proc while it runs (Fossil 2.27, 2026-10-13).A pull, push or sync can fail and exit 0. When the login no longer works, fossil syncs as nobody and prints
Error: login failed; when the remote cannot be reached, it makes no round trip (Round-trips: 0) and says so only on standard error (Fossil 2.27, 2026-10-06; OCX'sFossil.unsynced).What a clone says, fossil does: its
ssh-commandsetting runs when a pull reaches anssh://remote (Fossil 2.27, 2026-10-06). A session can write its clone, so OCX reads a run's clone only in a sandbox, never on the host (spec/ocx/fossil_spec.cr).The global
--user LOGINmakes a command's artifacts that login's, and Fossil records their receipt (rcvfrom) under it; a change pushed from a clone is recorded under the login that pushed it.A login with
pchanges its own password on the/loginpage, with the page's CSRF token and a same-originReferer; the old password is refused at once.fossil user passwordchanges only a local user table.Capabilities cannot limit a login to one branch, nor to audit tags only;
iadmits any write. OCX narrows what an agent may do with its own grants (Fossil's capability reference).
Forum posts
- No fossil command writes a forum post; only the forum's web page does.
fossil test-content-putstores an artifact, only in an open checkout, andfossil test-crosslinkindexes it, given its hash, though its usage says a record id: two commands, so a writer outside OCX holding the repository between them left a post stored and not indexed, which Fossil's rebuild would index, as the server's write of 2026-10-23 was left (ef10d7e83b; HISTORY.md decision 112). Pushed, the served repository indexes a post as one received from the pushing login (Fossil 2.27, tried on a scratch server, 2026-10-06). fossil bundle import B --publish --force --user Ustores, indexes and tags in one command: given a bundle holding a post (D, G or H, I, N, U, W and Z cards; a card's argument writes a space as\s) and the control artifact tagging it, or a ticket change and its tag, it stores each, public, indexes each (the post inforumpost, the change inticketchngand its ticket), applies the tag, and records their receipt underUwith no address, on the repository named outright or in a checkout, none needed; run again, it imports nothing more and succeeds. A bundle is an SQLite file whosebblobtable holds each artifact by its name, its size and its content as Fossil compresses one: four bytes of its size, most significant first, then zlib's stream.fossil bundle appendnames each by SHA1 whatever the hash policy, so OCX writes the bundle itself, naming each asfossil hash-policysays, SHA1 undersha1orautoand SHA3-256 otherwise, and Fossil checks each name against its content. Tried with Fossil 2.27 on a scratch repository, 2026-10-23, beside the adversarial review's trials of d9dd05e7dd; so OCX writes every post, ticket change and control artifact of tags this way (src/ocx/fossil/stored.cr).- A writer holding the repository fails a write at once: with another
connection holding its write lock, an open
BEGIN IMMEDIATE,fossil bundle import,fossil tag addandfossil uv addeach failed within 10 ms, storing nothing (database is locked: {INSERT INTO rcvfrom(...)}), astest-content-putandtest-crosslinkdid ({REPLACE INTO forumpost(...)}, the words of the server's failure of 2026-10-23): each reads in its transaction before it writes, and SQLite then fails it at once rather than wait. Fossil's busy wait of 15 s applies only to a command's first lock:uv addon a repository with no unversioned table yet, andpushon a clone so held, each waited 15 s and then failed.fossil test-xfer, the server's sync handler, whose transaction beginsBEGIN IMMEDIATEand so would wait itself, crashed (segmentation fault) given a push and a file card, a writer holding or not.fossil sql -R REPOSITORY ".timeout 15000" "BEGIN IMMEDIATE; ROLLBACK;"waits for such a writer to let go, by SQLite's own busy handling, and writes nothing: it returned as a writer let go, at 3 s and at 12 s, and without the.timeoutfailed (database is locked, exit 0) at 10 s, the sql shell's own wait (Fossil 2.27, 2026-10-23); past its.timeout, the writer still holding, it saysError: stepping, database is locked (5)and exits 0, after 2 s under.timeout 2000and at once under.timeout 0(Fossil 2.27, 2026-10-23). So a write that meets such a writer runs again once such a wait ends,BEGIN EXCLUSIVEin place ofBEGIN IMMEDIATE, as the next two say, the wait made outside OCX's lock beside the repository, and stores what one run would. It fails once writers or readers have held on past three waits (Fossil::TRIES), or have come first before each of eight runs after a wait that ended as they let go (Fossil::RERUNS): the wait gives the lock back before the run starts, and no write tried from Fossil's command line waits for the lock and keeps it (protocol v2 failures, case 35). - A reader holding the repository fails a write at its COMMIT, after
15 s, and only
BEGIN EXCLUSIVEwaits for it: with another connection holding an open read transaction (BEGIN; SELECT ...) and no writer anywhere,fossil bundle importfailed after 15 s, Fossil's busy wait at its COMMIT,database is locked: {COMMIT}, storing nothing (the adversarial review of 211a7d966b), whilefossil sql -R REPOSITORY ".timeout 2000" "BEGIN IMMEDIATE; ROLLBACK;"went through at once: a reader keeps out only the lock a commit takes, whichBEGIN IMMEDIATEnever asks for."BEGIN EXCLUSIVE; ROLLBACK;"asks for it, so it waits for readers and writers both: it returned as a reader let go, at 1 s, and past its.timeout, the reader still holding, saidError: stepping, database is locked (5)and exited 0, after 2 s under.timeout 2000and at once under.timeout 0. While it waits for a reader, new reads wait behind it: one starting 1 s into such a wait, the reader holding 3 s, answered at 3 s, as the wait went through (Fossil 2.27, 2026-10-23; HISTORY.md decision 114). - A checkout's own database is locked apart from its repository, and one
wait on both holds the repository while it waits: with another
connection writing
.fslckoutalone, an open transaction that inserted into itsvvar,fossil addandfossil committhere each failed at once (database is locked: {INSERT INTO vfile(...)},{UPDATE vfile SET mtime=...}), while a wait on the repository named by-Rwent through at once.fossil sqlrun in the checkout with no-Ropens the repository as its main database, then.fslckout(localdb), then Fossil's settings (configdb, which everyfossil sqlopens last, as the next point says), and itsBEGIN EXCLUSIVEtakes their locks in that order, keeping each it has while it waits for the next: with.fslckoutalone held, it took the repository's at once and kept it while it waited, so a read of the repository and the page/timelinetook 14.36 s (0.01 s beside a wait by-R); while OCX'saddthere met.fslckoutheld 70 s, 15 of 50 pages took over 15 s, the longest 29.99 s; and a person's command there holding.fslckoutand reading the repository, asfossil commitdoes, lost its write of the repository,database is locked (5)(the adversarial review of 39c646f7ad).fossil sql --no-repository :memory:run there opens no repository: an empty main database, then.fslckout, then Fossil's settings; naming.fslckoutitself as the main database opens it twice, and the wait waits on itself, failing at its.timeoutwith no one else holding (Fossil 2.27, 2026-10-23). So OCX waits for a command in a checkout first on that checkout's own database so, then on its repository by-R, Fossil's settings detached from each (the next point): as the add met.fslckoutheld 70 s, each of 50 pages answered within 0.01 s, the add failing past three waits after 45.1 s; and the person's write was kept, the add going through after 2.77 s (HISTORY.md decisions 115 and 116). The client's Fossil 2.27 opens no.fslckoutso, and the wait there waits on nothing (ticket fa798aee06), so these examples are pending there. - Every
fossil sqlopens Fossil's settings last, and a wait not detaching them holds what it opened first while they are held:fossil sql -R REPOSITORY, andfossil sql --no-repository :memory:in a checkout, open Fossil's settings (configdb,$HOME/.fossil) after the database a wait is on, and itsBEGIN EXCLUSIVEtakes their lock last, keeping the other's while it waits, so while another of the account's commands held the settings, afossil sqlsession reading or writing them, orfossil commit, which writes them in its transaction, the wait kept the repository, or.fslckout, from every reader: with a reader holding the settings 10 s, a wait by-Rtook 9.75 s, and a read of the repository, the page/timelineandfossil statusin its checkout each 9.15 s; as a report write met a writer outside OCX holding the repository 3 s, another session in OCX's home holding the settings 20 s, pages waited up to 11.95 s; and as OCX's add in a checkout waited for a writer holding its repository under OCX's home, a person'sfossil changesthere took up to 13.45 s (the adversarial review of 4228a3de0b).DETACH configdb;beforeBEGIN EXCLUSIVEleaves the wait on its one database: each of those took 0.01 s then; it goes through under a home holding no settings yet, which the shell opens all the same; and with a writer holding the repository 3 s, the wait still returned as it let go, at 3.04 s. A writer holding the settings alone meets only a command that writes them, which waits for it itself: with one holding them 5 s,fossil commitwent through at 5.05 s, andfossil addandfossil bundle importat once (Fossil 2.27, 2026-10-23; HISTORY.md decision 116). - Every fossil command reads what it opens as it starts, a wait before
its
DETACHtoo, so one keeping their new readers out holds each, 15 s at each of two reads, three for a checkout's own database: with a reader holding Fossil's settings and a writer of them, asfossil commitis, waiting at its COMMIT,fossil add,fossil bundle importand a wait by-Reach failed after 30.03 s,database is lockedatSELECT value FROM global_config(the adversarial review of aa650c80c4); with the repository so held, so did a wait by-R,fossil info, andfossil addand the wait in its checkout,not a valid repositorylast; and with a checkout's own database, itsfossil add,fossil changesand wait, after 45.08 s. A clone by URL that met a writer of the settings failed after 15.03 s, Fossil taking away the file it made; one of a repository file, which OCX never makes, left its own (Fossil 2.27, 2026-10-23; HISTORY.md decisions 117 and 119). - Any push needs Check-In (
i), a forum post's too: a login with the forum's capabilities alone getsError: not authorized to write. Pulling needs Read (o) alone: a clone made by a login holding onlyoholds the tickets. A write on the repository itself needs no capability at all: a login holding none wrote a ticket change with--user, and the receipt names it, with no address (Fossil 2.27, a scratch server, 2026-10-12). - A clone records everything it pulls as received from its own login,
so who wrote a post there is only its U card; on the served repository,
rcvfromnames the login that sent it. The clone's login holds setup (s) in the clone, and a pull's receipt carries the server's address, as a push's carries the client's; a write on the repository itself has none (Fossil 2.27, a scratch server and clone, 2026-10-12). - A repository has no
forumposttable until its first post.
Check-ins, tags and comments
fossil commitrefuses a comment containing<…>as possible HTML, such as an e-mail address in a trailer, unless given--no-verify-comment(d26240540f).fossil amend --commentreplaces the comment shown and read; the original stays in the check-in's manifest and can be recovered from it (877097e6b9).fossil commit --branch NAMErefuses a name an open branch has (an open branch named "NAME" already exists - use --force to override), so a unit's first commit starts its branch only while no one else has;fossil branch currentsays which branch a tree is on (Fossil 2.27, 2026-10-06).- A tag added again replaces the earlier value, so a corrected
auditedverdict supersedes the first while history keeps both (6912141fbe). fossil tag add --raw TAG CHECK-IN VALUEnames the tagTAGitself, notsym-TAG, so it never becomes a name for the check-in, and on that check-in alone (itstagxrefrow's type 1), unless--propagate; the check-in comes before the value. Written on the repository itself with--usernaming a login that holds no capability, it is received from that login with no address, and its change is aneventof typegnaming it (Fossil 2.27, a scratch repository, 2026-10-12).
Git export and import
What Fossil's git export and import do, as OCX has learned them, is in docs/fossil-git.md.
What the daemon relies on
The facts spec pins Fossil 2.27; source lines refer to it.
- The first
/chat-pollmakes the chat table. Before it, no event adds a message. The daemon relies on this: then each local or synced timeline event adds one fromchat-timeline-user; approval adds none. Ids are never reused (example 1;alerts.c158–215;chat.c273–285). - Chat polls wait for a message or timeout.
/chat-pollchecks about once a second, returning newer messages or emptymsgsatchat-poll-timeout. Without a same-origin Referer,name=0gets HTTP 400, “Database error: not authorized” (example 2;chat.c713–787;main.c2197;cgi.c706–731). A start usesname=-1: only the newest message, or every message when fewer than two exist (the newest-message example;chat.c744–749). - One pull gives every artifact one receipt, across round trips
(example 3;
content.c472–482). - Only an error-free pull saves its remote. Transfer failures exit 0:
an unreachable host prints “cannot connect to host 127.0.0.1:1” and
“Round-trips: 0”; a wrong password prints “Error: login failed”.
remote add NAME defaultcopies URL and password. Afterremote offdeleteslast-sync-*, a pull finds its user only from the clone's default user (or USER, which OCX never passes); with neither it exits 1, “cannot determine user”. The daemon's clone therefore names the node user as its default (fossil user default) (example 4;sync.c71–72, 397–411, 659–668, 700–717;user.c556–589). - Fossil's receipt pages require Admin.
/rcvfromlistand/rcvfromrefusegoC4w; OCX's read-onlyreceiptpage gives that user the sender, capabilities, receipt and row (example 5;shun.c355–366, 468–475). - The node writes through pages, syncing verdicts and shares.
With
goC4w,/forume2and/tkteditstore public replies and changes before redirecting, using one user's CSRF token from/forume1on both pages and a same-origin Referer (login.c57–68;cgi.c772).4skips moderation and implies3and2;wimpliesr,nandc. Tickets usemutype=Markdownto setmimetype=text/x-markdown, ignoring postedmimetype. Withiandy, a tag bypush --onceand a file byuv syncleave the default remoteoff; plainpushsaves it (example 6;forum.c1274–1279, 1404–1411;login.c1726, 1737–1739;tkt.c1131;tktsetup.c678–693;sync.c263, 524–530).
SQL
Each node's
fossilbrings its own SQLite, bundled or the system's: this host's Fossil 2.27 runs SQLite 3.51, and the BeagleY-SSD node's refused an aggregate's ownORDER BY(json_group_array(... ORDER BY ...)), which SQLite takes only from 3.44 (the maintainer, 2026-10-07). OCX orders rows in Crystal instead, by keys each row carries, and the gate's ledger prints the SQLite itsfossilruns on.fossil sqlreports an SQL error on standard error and exits 0 (Fossil 2.27):ocx2counts anyError:there as a failure.
Markdown
- Headings get no anchors. A link to a section needs an explicit
<a id="…"></a>in the heading, which survives safe rendering (5bbde981d9).