Changelog¶
Unreleased¶
- Changed (behaviour, dependencies): carrel now requires
pypdf>=6.19.0(was>=5.0). carrel reads PDFs it did not write through pypdf —edit,form,note,sign,audiobook, and the page counts ininspectanddiff— and pypdf 6 hardens its parser against hostile files release by release. The two newest: 6.18.1 tightens FlateDecode recovery and caps/Widthsentry counts andparse_bfchartoken lengths, and 6.19.0 fixes three advisories, one of whichform fillreached (GHSA-php9-fj8v-98fj): pypdf builds an appearance for every text and choice field it fills, and on 6.18.1 a list box offering 160,000 options, a 1.5 MB PDF, heldform fillfor over two minutes against about a second now. The other two are in code carrel does not call (embedded files, GHSA-v247-6f48-mgcj; page labels, GHSA-w23x-9jrw-r45c). A floor of 5.0 letpip install carrelkeep an older pypdf already in the environment, so those fixes reached carrel's own test lock and not its users. One visible side effect comes with pypdf, not carrel:form fillnow paints each field's own/MKbackground and border in the appearance it generates, and the text clip box shrinks by the border width. It is a behaviour change under D-023 because pypdf 6's limits refuse some files 5.x read (D-026). - Fixed: a PDF pypdf's parser refuses — its own
PyPdfErrors,LimitReachedErrorincluded — exits 4 inedit,form,noteandsignwitherror: unreadable PDF: …(notenames the file), or the--jsonerror object, instead of exit 1 andunexpected error: …;inspectanddiffkeep folding a read failure into their report. pypdf 6 reports its hardening limits asLimitReachedError, which is not aPdfReadError, so the one command that caught pypdf errors (note) missed it and the others caught nothing. An encrypted PDF says to decrypt it withedit pdf --decrypt, and pypdf'sDependencyError(AES withoutcryptography, JBIG2 withoutjbig2dec) exits 3 with pypdf's message naming what is missing. pypdf's own log lines no longer reach stderr unless--debugis given: a 156-byte PDF with no/Rootwrote 50,000 before the error. An error that reachesmainitself is JSON under--jsontoo. Still exit 1: malformed structures that make pypdf raise a plainValueErrororTypeError(STATE.md). - Added:
context7.jsonstates what Context7 indexes and how agents should use carrel. It was serving carrel's docs to other people's coding agents with no configuration at all. Scope isdocs/andplugins/; fiverulesgive an agent something checkable (--jsonplus the exit-code table,carrel doctorfirst,pack --queryand its non-zero empty,carrel mcp's confinement, the dry-run guard). Both exclusion lists replace Context7's defaults rather than adding to them, so the defaults worth keeping are re-listed — and the default*archive*folder pattern is deliberately dropped, because it was excluding the shippedplugins/carrel-mail/skills/mail-archive.projectTitleanddescriptionfollowproduct.jsonthroughscripts/sync_product.py, with tests and theproduct-synchook. The rules are tested against the real CLI: every command and flag a rule names must appear in its--help, and only commands that call the tracked-files guard may be said to have it — the first draft claimeddedupedid, and it does not (D-025). - Added:
.github/workflows/context7-refresh.ymlasks Context7 to re-index after the documentation changes. Written from the published OpenAPI spec —POST /api/v1/refresh, bearer auth,libraryNamederived from the repository — not from guesswork. It logs the HTTP status and the documentedmessagefield only, never the raw body or anerrorfield, which is where an authenticated endpoint would echo a credential. Green and skipped untilCONTEXT7_API_KEYexists, so there is nothing to disable in the meantime.
v0.5.0 — 2026-09-12¶
- Changed (behaviour,
redact):--builtinis repeatable, like--pattern. It took one comma list, so a second--builtinsilently replaced the first —--builtin email --builtin ibanredacted IBANs and left every address in place, reporting success.--builtin a,b --builtin cand--builtin a,b,care now the same thing, and a repeat is deduplicated rather than applied twice. - Changed (behaviour,
carrel mcp): the server is now confined to the directory it was started in (--root, else the working directory).SECURITY.mdalready listed "the MCP server reading or writing outside its root" among the reports it cares about most, and the server did exactly that: started indocs/, it returnedcarrel_inspectmetadata for/etc/hostnameand served/tmp/…/secret.txtthroughresources/read. Every path a client can name — each tool'spath/paths/out_dir, the per-callroot, and bothcarrel://URIs — now resolves through one helper that follows symlinks and then refuses anything outside the root:isError: truewith exit code 2 for tools, resource-not-found for resources. A per-callrootcan therefore narrow the desk but no longer leave it.carrel mcp --allow-outside-rootrestores the old behaviour for a session, andcarrel --root / mcpis unconfined by construction because/is then the desk you named.plugins/carrel-agent/.mcp.jsonis unchanged — Claude Code starts the server in the project directory, which is the desk (D-021). The tools that walk a tree —carrel_pack,carrel_index,carrel_refs,carrel_fieldsandcarrel_mail action=threads— are confined too, and that took more than checking the path the client named: the walkers skipped symlinked directories but still read symlinked files, so a link planted in a desk was a way out of it, andcarrel_indexthen stored the contents wherecarrel_searchwould serve them. A walk started by the server now drops any entry that resolves outside the root. The CLI is unchanged and still follows links, because a desk that symlinks documents in from elsewhere is a legitimate layout — which is also why the tools that return stored paths (carrel_search,carrel_tag find,carrel_meta find) filter their rows: a desk indexed from the shell can hold rows pointing anywhere,--prunekeeps them because the target still exists, and a confined server was serving their paths and their text. Writes are confined too, and that needed the other half of the rule: a write follows a symlink and its destination is derived, never named by the client, so a link planted wherecarrel_convertorcarrel_mail attachmentslands carried the write outside the root — and a dangling link created the outside file with no existing file to force past. Every derived destination is now checked where it is computed — including<root>/.carrel, where the desk database lives: a symlink there sent the index (the extracted full text of every file in the desk) and every tag, note and field outside the root, andcarrel_searchread it back. - Changed (behaviour): the tracked-files guard on
rename,organize,intakeandwatchis overridden by--allow-tracked.--forcemeans "overwrite existing output" on seven other commands, and those four never overwrite anything — so reaching for it by reflex disabled the guard that exists because arename --applyonce renamed 21 tracked files in this checkout.--forcestill works on all four and is not being removed; it prints one stderr line saying it now means--allow-trackedand that the spelling is deprecated. No removal date is set. The warning fires when the guard is actually consulted — a dry run is never guarded, so--forcestays silent there rather than claiming a bypass that did not happen (D-022). - Changed (behaviour,
carrel-guard): imageReads pass through to Claude by default. The guard OCR'd every.png/.jpg/.jpeg/.icounconditionally, replacing a picture Claude can already see with a transcription — worse for a screenshot, a chart or a photo, and with no way to turn it off.CARREL_GUARD_OCR_IMAGES=1restores it. PDFs are still converted to text by default, because page images cost far more tokens, butCARREL_GUARD_PDF_TEXT=0now hands them back to the visualReadwhen layout or diagrams matter. - Changed (behaviour): a
carrel packthat found no files now prints one line on stderr naming the reason, and under--jsonit exits 5 instead of writing a valid, empty document. FTS5 AND-s the terms of a--query, so a natural-language question usually matches nothing — the failure a caller is least likely to notice, because an empty pack is indistinguishable from a successful one. "No files" means none reached the pack: a directory of images, a--tree-onlyrun and a--max-file-bytesthat skipped everything all still produce a useful listing and still exit 0, and a--sincewhose only change was a deletion reports the deletion rather than failing. The message names the filter that actually emptied the result.--no-fail-emptyrestores exit 0; human mode still exits 0 by default and--fail-emptyopts in. Scripts that pipepack --json --queryshould either fix the query or pass--no-fail-empty.PackResult.empty_reasoncarries the same sentence, so the MCPcarrel_packtool reports it too — an agent has no exit code to read (D-018). - Changed (docs): the README leads with what carrel does, not with a tour of the TUI. The
first screen was a
desk-tour.gifand a paragraph about study carrels; the people arriving are Claude Code users with a folder of PDFs. It now opens with the functional line — Read, index, pack and file your documents — from the terminal, for you and your agents — thenpack.gifandredact-proof.gif, one paragraph, install, and three things to try: give Claude the right context, read what the agent can't, turn an inbox into an archive. Each has a command block that was actually run and one honest limitation. A Status and support section says what is stable, what is experimental, which platforms CI really covers, and two things carrel is not.desk-tour.gifmoves to "The desk TUI", where "the flagship" becomes "a companion to the CLI". Nothing was removed — all 33 command rows, all 9 plugin rows and every link stay, checked as a set diff over rows, command mentions, images, full link text and headings. The first version of that check compared link targets and no images, and so missed theassets/logo.svgmark being dropped from the body and a link whose text had been gutted — both caught in review, both restored.docs/index.mdgets the same first screen, andproduct.json's description starts with the functional line, so it reachespyproject.tomland the PyPI summary; it does not reachCITATION.cffor the plugin manifests, whichsync_product.pyonly version-stamps (D-024). - Changed (stderr format): under
--json, an error is now itself one line of JSON on stderr —{"error": "...", "exit_code": N}— instead oferror: .... stdout is unchanged and still the data channel. A caller that pipes--jsonhad to parse English out of stderr to learn anything beyond the exit code, and the exit code is what a shell sees, not what an MCP client or asubprocessreading stderr does. Every error carrel itself prints goes through one formatter —fail, the top-level handler incli.py, and the per-file loops inconvertandthumbthat report an error per source and keep going. The one exception isclick.UsageError(a malformed command line, exit 2), which click renders itself with theUsage:banner that is the answer to it. Scripts that grep stderr forerror:under--jsonneed updating; without--jsonnothing changed. - Added: PyPI
classifiersandkeywords. The package had neither, so it was unfindable by topic and its metadata page said nothing about what it runs on.Operating System :: Microsoft :: Windowsis deliberately absent untiltest-minimal (windows)is a required check — a trove classifier is a support claim. - Fixed: install hints name the package manager you actually have. All nineteen adapters
carried a Debian
aptline, socarrel doctoron a Mac told you tosudo apt install tesseract-ocr— advice that cannot work — andtest-minimal (macos)is a required check, so that platform is supported and was being mis-served. Each adapter now carries anapt,brewandwingetpackage name and the hint is chosen at render time: a manager onPATHfirst, else this platform's own. Where no manager packages the binary the hint says so and gives the vendor page (icotool,readpston Windows); where the tool is a Python package it falls back topipxonly on the platform with no native package. Every package name was resolved against the real registry —formulae.brew.shandwinget search— because a confidently wrong package name is worse than no hint. A Linux with no manager we recognise is not told to useapt:sys.platformislinuxfor Debian and Fedora alike, so a box withoutaptonPATHgets the package's name under each manager instead.docs/INSTALL.mdgains the same data as a table, and the six plugin docs that told an agent to relaysudo apt install …now tell it to relay the hint carrel printed. A test fails on anyapt|brew|winget|dnf|yum|pacman|…install command in shipped code or a plugin doc. - Fixed:
README.mdclaimed 855 tests and ten cookbook recipes,docs/CONTRIBUTING.mdclaimed 501 tests; both were wrong by hundreds. A count nobody regenerates is wrong within a week — this bullet first stated one of its own and got it wrong, which is the argument — so the live docs no longer carry them, andtests/test_docs_drift.pyfails on anyN testsorN recipesclaim outside the dated records inHISTORY. - Fixed:
docs/index.mdlisted the supported types without eml/mbox whileREADME.mdincluded them. - Fixed:
.gitignorehad no.idea/. - Fixed (
carrel mcp): a JSON-RPC message whoseparamsis an array — legal per JSON-RPC 2.0 — took the whole server down mid-session with anAttributeError, because every handler readsparamswith.get(). It is now a-32602error like any other bad request and the session continues, which is what the module has always promised. - Fixed (
carrel mcp):initializeno longer echoes whateverprotocolVersionthe client sent, which claimed support for any string a client cared to invent —2099-01-01came back as2099-01-01. The server keeps a tuple of versions it actually speaks (2025-06-18,2025-03-26,2024-11-05— its JSON-RPC surface is identical across them), echoes a requested version in that tuple, and otherwise answers with the newest it supports, which is the MCP spec's rule. - Fixed (
carrel-guard): the README claimed Claude'sRead"cannot parse PDFs, Word/ OpenDocument/EPUB/RTF files, spreadsheets, email files or images". Per the tools reference it reads images as pictures and PDFs natively; the claim holds only for.docx .odt .epub .rtf .xlsx .eml .mbox .mbx. The plugin README, the README marketplace row anddocs/MARKETPLACE.mdall say what is true now. - Fixed (
carrel-guard):CARREL_GUARD_TIMEOUTdefaults to 15 s, up from 5.carrel convert --to txttakes 6.7 s on a 68 KB pandoc-written docx and 13.9 s on a 127 KB one, so the old budget killed ordinary documents — and did it silently, because a timeout exited 0 with no output and Claude saw only a Read that happened to be slow. A timeout now reports itself and lets the Read proceed on the original:additionalContextwithoutupdatedInput, which the hooks reference allows, the decision fields being independent. Every conversion note also says where the original still is, and when to prefer it. - Fixed:
packnow honours the worktree root's.gitignorewhen packing a subdirectory.ancestor_ignoresreturns nothing when itstopequals itsstop_at, andpackpassed the packed arguments' common path asstop_at— which for a single directory argument is that directory. Socarrel pack src --stats --tree-onlylisted 45__pycache__entries from this repo whilecarrel pack .listed none: the README's ownpack.gifcommand, packing build artefacts into a context window.packpasses the desk root now (--root, default the cwd), which is whatindexalready passed. The v0.3.1 guard is untouched by design — nothing above the declared scope is consulted, so auv venv's.gitignoreof*still cannot blank a desk, including the usual case where the venv sits inside a checkout (D-019).assets/demo/pack.gifis re-recorded from the fixed build. - Fixed (
watch --print-service): the generated systemd unit now setsWorkingDirectory=to the directory carrel was invoked from, so the unit reproduces the invocation. A systemd user unit starts in$HOME, so a--runaction holding a relative path —mv {path} "archive/$(date +%Y-%m)", the shape the v0.4.1 entry advertises — filed every processed document into~/archive/instead of where an interactive run would put it, silently and forever._abs()only ever absolutised the option values; the action template was the one relative path left, and it has to resolve against the same root they do. - Fixed (
watch --print-service systemd): a watched directory whose name contains a newline can no longer inject directives into the unit.Description=was emitted bare, so everything after the newline became further unit settings in the file the user is told to save andsystemctl --user enable— an attacker-chosen[Service]/ExecStart=among them. A name ending in a backslash was the quiet version: systemd's line continuation swallowed theAfter=default.targetline that followed. Both are now refused with exit 2, because a unit file cannot represent either. Verified against systemd 259 rather than against a model of it: quoting these settings does not work — a quotedWorkingDirectory=is rejected as "path is not absolute", and a quotedDescription=keeps its quotation marks verbatim. - Fixed (
watch --print-service): the unit file name,Description=, the systemctl hints and the schtasks/TNall read the product name fromproduct.jsoninstead of hardcoding it, as CLAUDE.md requires._launcher_pathcomparedargv[0]against a literal"carrel", so after a rename it silently fell back topython -m carrel.cli— a module that no longer exists — and every generated unit died at boot. - Fixed (
watch --print-service): anargv[0]with no final component (/,., or any path ending in a separator) raisedValueErrorout ofpathlibinstead of a clean message; and the launcher path is normalised without being resolved, so anargv[0]carrying..no longer produces anExecStart=systemd refuses ("Executable path contains special characters") while still naming the Homebrew/Nix symlink rather than the store path behind it. - Fixed (
watch --print-service schtasks): the printed line warns when the/TRvalue exceeds the ~261-character limit schtasks truncates or rejects at, and the cmd.exe metacharacter warning now names every part of the line — the watched path,--done-dir,--error-dirand--log, not only--run.C:\R&D\inboxis enough to split the paste. - Fixed (performance): the
watchtracked-file guard prunes.gitand the--done-dir/--error-dirsubtrees while walking instead of filtering afterwards. On a 200-file repository with a 150-file archive the walk went from 425 paths to 200 — 52% of the old set was.git/objects, every one of them stat'd, resolved and sent throughgit ls-filesfor an answer git can never give — and it no longer grows without bound as the archive fills. Hidden files stay in the guarded set:--existingskips them, but a live event applies only--glob, so a committed.gitkeepreally can be filed away.--existingshares that walker now, so it gets the same pruning — and a relative--done-dirprunes like an absolute one, which it did not when the two walkers were separate.core.fsopsalso stopped resolving every path twice (~850 realpath walks for 425 files).
The problem. A new user's first five minutes with 0.4.1 hit four real defects and one
false security claim: carrel pack src packed 45 __pycache__ entries because the worktree's
own .gitignore was ignored; a --query that matched nothing exited 0 with an empty document;
the guard OCR'd screenshots Claude could already see; install hints said apt on every
platform; and SECURITY.md promised the MCP server was confined to its root when nothing
confined it at all.
Proof. carrel pack src --stats --tree-only | grep -c __pycache__ — 45 before, 0 after.
A confined carrel mcp started in a scratch docs/ answers
resources/read carrel://file//tmp/…/secret.txt with resource not found and
carrel_inspect on the same path with isError: true, and the file's contents appear
nowhere in the output. A .png Read payload through read-guard.sh produces nothing and
exits 0. Five review rounds on the boundary alone found five distinct ways a path reached
the filesystem — named by the client, found by a walk, derived by a writer, read back from
the index, and the index's own location — each now closed and each with a test that fails
when the fix is reverted.
What remains imperfect. fields accuracy is unmeasured — English-label heuristics with a
confidence column, no corpus behind it. The boundary is enforced by threading a flag to ten
call sites rather than by one confined filesystem accessor; that refactor is the first item of
v0.6.0 and is why five rounds each found "one more caller also does this". Windows CI is
advisory, and the required macOS check is the no-extras degradation job, so the macOS
conversion paths are exercised by nobody.
Try it. uv tool install 'carrel[all]' && carrel doctor
v0.4.1 — 2026-09-11¶
- Changed (behaviour):
rename --apply,organize --apply,intake --applyandwatch --done-dir/--error-dirnow refuse to start when the move would touch a file git is tracking, exiting 2 with the repository and the offending paths named. Each gains--forceto override. Untracked files inside a repository are fine —~/Downloadsunder a dotfiles repo keeps working — and the dry-run default is never guarded.intakechecks INBOX and--toand refuses before creating--to, so a refused run leaves the disk untouched;organizealso checks--intodestinations, which can climb out of DIRECTORY. This exists because on 2026-09-10 arename --applyaimed at carrel's own checkout renamed 21 tracked files after the "fields" it read out of their source; the command was correct and the outcome was still wrong, because a tracked file's name is content. Without the git binary carrel cannot tell what is tracked, so it exits 3 with git's install hint rather than guessing (spec 29, D-017). Only paths the command could really move count:watchskips files its--globcan never match, a tracked symlink counts as the link rather than its target, and file names containing*,?or[are matched literally. If you script one of these against tracked files, add--force. - Fixed (Windows): every text read and write now names its encoding.
text=TrueonsubprocessandPath.read_text()both fall back tolocale.getencoding(), which is cp1252 on a stock Windows box — so every external tool's output (pdftotext,pandoc,tesseract,git) was being decoded as cp1252, and a PDF containingcafécame back ascafé. CI never noticed because it setsPYTHONUTF8=1.ruff'sPLW1514is the first gate andtests/test_text_encoding.pythe second, because that rule only fires where it can infer aPathreceiver; 10 of the sites were beyond its reach, including the write that recreatesproduct.jsonand would have truncated it to 0 bytes on a non-UTF-8 locale. - Fixed:
carrel mcpnow forces UTF-8 on its stdio. MCP frames are UTF-8 by specification, but Python wires stdio to the locale encoding witherrors="strict", so a document containing CJK or an em dash could kill the server mid-session on Windows. - Fixed: reading a user's document goes through one place (
utf-8-sig,errors="replace"), so Excel's "CSV UTF-8" export no longer names its first column\ufeffname, and its plain cp1252 export converts instead of raising. Four readers had drifted to three different error policies. - Fixed: generated output that gets hashed —
pack,sign manifest,catalog export— is written withnewline="\n". On Windows the same source tree produced CRLF and therefore a different sha256, whichdiff --mode bytes, catalog hashes andsign manifestall read as a changed file. The files carrel generates for itself (_product.py, the manifests) are pinned the same way, matching the other sync scripts. - Changed: the
@handleddecorator (CarrelError → message + exit code) and theroot_ofdesk-root resolver live once, incarrel.core.output(D-016). They had 25 and 12 byte-identical copies across the command modules, four more root lookups open-coded, and three modules carrying the decorator's body inline;colorimportedproof._handledacross modules. Behaviour is unchanged.handledis now generic in the wrapped signature, so mypy checks calls through it, anddebugging(ctx)is the one place the global--debugis read. - Fixed:
watch --print-servicewrote the unresolved--root,--done-dir,--error-dirand--loginto the generated systemd unit andschtasksline. A service starts in the manager's working directory —$HOMEfor a systemd user unit — so a relative--rootmade the unit fail on every start (--rootrequires an existing directory), and a relative--done-dirwould have filed documents into a directory under$HOME. Every path in a generated service is now absolute. - Fixed:
watch --print-serviceoutput now survives real actions. The systemd unit'sExecStartuses systemd's own quoting — backslashes and quotes C-escaped,%and$doubled — so--run 'mv {path} "archive/$(date +%Y-%m)"'reaches the service intact instead of%Ybecoming the unit directory and%mthe machine ID; verified by round-tripping argv through a real systemd unit. Theschtasksline quotes the command once for carrel and again as the/TRargument, because Windows parses it twice, so embedded quotes and trailing backslashes survive. Paste it into cmd.exe, not PowerShell, and not when an action contains&,|,<,>,^or%. Both now name the carrel launcher that printed them rather than the firstcarrelon PATH — kept as the PATH symlink rather than resolved into a version directory an upgrade deletes, and found on Windows although the launcher hides its.exe— and carry the global--jsonas--json-lines. - Fixed:
docs/index.mdclaimedcarrel mcpserves "ten MCP tools" — it has served 14 since v0.4.0.tests/test_docs_drift.pynow scans every live doc, and the plugin skills whose frontmatter states it, for a tool count in any spelling (numeral, word, hyphenated, emphasised) and checks it against theTOOLStable incarrel.commands.mcp; it also pins every tool name indocs/AGENTS.mdand the inline lists in README anddocs/FEATURES.md. Historical counts in the FEATURES release trail are left alone.docs/REPO_SETTINGS.mdis pinned againstREQUIRED_CHECKSin the hardening script the same way.
v0.4.0 — 2026-09-10¶
The accounting inbox. carrel reads what a document says, links documents by the numbers they share, files them where they belong, and answers questions about them — and email is a first-class file type throughout.
- Added:
carrel meta— typed key/value fields on desk files (set/get/ls/rm/find/export), stored in the new schema-v2metatable. Kinds (str/num/date/bool) are inferred and values stored canonically, someta find total>1000compares numerically anddue<2026-11-01chronologically;meta exportwrites the desk as a CSV/JSON table;search --meta CONDfilters hits;catalog export/importcarry fields with tags and notes (schema 2; schema-1 documents still import). Existing desks migrate on open (D-009). - Added:
carrel refs— find reference numbers in any supported file: label-driven invoice/PO/order/check/account/tracking/ticket kinds and check-digit-verified IBAN, ABA routing, EIN, VAT, ISBN, GTIN, DOI, UPS and USPS identifiers, with page numbers for PDFs.--tagwritesref:<kind>:<value>tags sotag find/search --taglink the documents that share a reference;--linkprints that grouping;--pattern NAME=REGEXadds house formats. The kinds live in the newcore/patterns.py. - Changed:
redact --builtinaccepts every kind of the shared registry (iban,routing,invoice, …); label-driven kinds replace only the value, soInvoice # ████keeps its label. - Added: email as first-class files —
.emland.mbox(.mbx) areFileTypes read by the standard library (core/mail.py), soinspect,convert(eml → md/txt/html/pdf, mbox → md/txt),index/search --type eml,pack,diff,organize(→mail/),refsand thecarrel-guardRead hook all handle them. Shape sniffing applies only to unmapped extensions (D-012). Outlook.msgis cut (D-011);.pstcomes in through readpst. - Added:
carrel mail—attachments(saved with sha256, names sanitised, never overwritten without--force),split(mbox → dated.emlfiles),threads(Message-ID / In-Reply-To / References),pst(Outlook exports via the newreadpstadapter,sudo apt install pst-utils). - Added:
carrel fields— vendor, invoice number, PO, dates, subtotal/tax/total, currency, IBAN and account last-4 out of invoices, receipts and statements (label heuristics over the text spine,core/money.pyandcore/dates.py), each with a confidence and its evidence line;--setoverrides,--savewrites them as desk fields. - Added:
carrel rename --template '{date}_{vendor}_{ref}{ext}'— names from the document's own fields (or desk fields / references), slugified, dry-run by default, never overwriting; the desk row follows the file.organize --applymoves through the same helper, so tags and notes no longer go missing after a move (pre-v0.4.0 bug). - Added:
carrel batch— run shell actions over many files withwatch's{path}language (core/actions.py, D-013):--jobs,--dry-run,--manifest+--resume,--fail-fast,--json-lines; exit 1 when any file failed. - Added:
carrel watchupgrades —--recursive,--existing,--stable SECS(wait for a file to stop growing),--poll(for/mnt/cand network shares),--done-dir/--error-dir,--log FILE,--print-service systemd|schtasks;{stem}and{ext}substitutions. - Fixed (email, found by review before release): converting a message to PDF now renders its
text, never the sender's HTML, so a conversion can no longer fetch a tracking pixel, reach an
intranet URL, or embed a local
file://into the output;eml → htmlre-declares the encoding it is actually written in (no more mojibake from awindows-1252header). A malformedDate, an address header containing a newline, an unknown charset, a 3000-message reply chain, a 300-character attachment name and a Windows device name are all handled as data instead of crashing or silently dropping content. Attachments nested inside forwarded messages are found; two attachments with the same name in one run no longer overwrite each other;mail splitplans every name before writing (so a collision cannot leave half a mailbox on disk) and writes the bytes as stored, keeping encodings and DKIM signatures intact.mail pst --format mboxpasses readpst-r(onemboxper folder) rather than-M(MH format). The mail shape-sniff now applies only to files without a real extension (D-012), so.patch,.diffand log files are no longer reclassified as mail, while Maildir names still are; a mailbox saved as.emlis detected by its bytes instead of swallowing every message after the first. - Fixed (extraction and automation, found by review before release): a negative amount on a
labelled line kept its sign (
Total Due -$1,234.56was booked as a charge), a decoy label line no longer wins over the real one (Total units 3.00aboveTotal $1,234.56,Tax ID:aboveTax),Net 500.00is read as a subtotal rather than 500-day payment terms, a dotted date (2026.03.04) is no longer harvested as the amount 2026.03, and a currency tie resolves the same way on every run.watch/batchaction templates are substituted in one pass, so a file named{name}.txtcan no longer produce a command that acts on a different path;--manifestand--logcreate their directories instead of crashing on the first record;watch's output-name guard only suppresses added segments (report.pdf→report.txt), so a new input that merely shares a prefix (report-2026.pdf) is no longer lost;--existingskips--done-dir/--error-dir, so a restart does not re-run every action over the archive; andrename --applyrecords a failed move instead of discarding the whole plan. - Added (MCP):
carrel_meta,carrel_refs,carrel_fieldsandcarrel_mailtools (14 tools);carrel_searchtakesmeta. - Added (marketplace):
carrel-financeplugin (/refs,/fields),carrel-mailplugin (/mail+ amail-archiveskill);/meta,/rename,/batchincarrel-organize.
v0.3.2 — 2026-09-10¶
- Fixed (Windows):
carrel watch --action-timeoutcrashed withmodule 'os' has no attribute 'killpg'when an action timed out; a timed-out action is now killed as a process tree (taskkill /T) there.CARREL_BIN_<NAME>overrides counted any existing file as executable on Windows (os.access(X_OK)is plain existence there); executability now followsPATHEXT, so a stale override never silently resolves (D-008). Substitutions inwatch --runtemplates are quoted for cmd.exe rather thansh. - Fixed: notes on a file come back newest-first by insertion order as well as timestamp, so two notes added within one clock tick keep their order.
- Changed:
carrel searchprints bm25 scores with three significant digits (score -0.0412), matchingpack --stats, instead of rounding them all to-0.00.
v0.3.1 — 2026-09-09¶
- Fixed: an unrelated
.gitignorein a distant ancestor directory could silently exclude an entire desk. The ancestor walk had no stopping point outside a git repository, so it collected rules all the way to/. It now stops at the repo root (.git) or the caller's root — the desk root forindex, the common root forpack— and an unbounded walk contributes nothing. Found while verifying v0.3.0 from PyPI:uv venvwrites a.gitignorecontaining*into the venv directory, and a desk created inside one reportedindexed: 0, skipped: 0, errors: []with nothing to explain it.packwas affected the same way and is fixed by the same change.
v0.3.0 — 2026-09-09¶
The desk reads code. carrel index covers source trees, so the agent-facing
half of v0.2.0 — pack --query, search, and the plugin reindex hook — finally
works on the repositories agents actually point it at.
- Added:
carrel indexcovers plain-text source and config files (.py,.rs,.toml,.yaml,Makefile, …) as the new typecode, sosearch,pack --query,tag,noteand the MCPcarrel_search/carrel_packtools reach source trees — closing the "planned follow-up" left open in v0.2.0.search --type codefilters to them;--no-sourceopts out. - Added: the
indexwalk honours.gitignore(--no-gitignoreopts out), sharingpack's matcher via the newcarrel.core.ignore. Without it, indexing a repo would pull innode_modules/,build/anddist/. - Changed: source files are indexed by default. An existing desk will
grow on its next
carrel indexrun; pass--no-sourceto keep it to documents..gitignoreis honored by the same walk, so ignored build output stays out. - Changed:
carrel diff a.py b.pypickstextmode automatically instead of exiting 4;carrel inspectreports source files astype: codewith a line/word/char detail block. - Fixed:
DeskDB.rel()and PDF-manifest entries now use POSIX separators.files.pathis written verbatim intocarrel catalog export, so a catalog produced on Windows could not be imported on Linux — the exact portability D-009 exists to provide. - Fixed: the
carrel-agentPostToolUse(Write|Edit)reindex hook was a silent no-op on source repositories, because the files Claude writes were never an indexable type.
v0.2.0 — 2026-09-04¶
The desk grows up for agents: the whole CLI is reachable over MCP, pack can
select by relevance or by git history, the index is versioned and portable,
and Office/ebook documents join the supported types.
- Added: docx, odt, epub, rtf (via pandoc) and xlsx (via
carrel[office]) acrossconvert,inspect,index/search,pack,diff;convert --sheetfor workbooks; md/html/txt → docx/odt. - Added:
carrel pack --query TEXT [--top N](index-ranked packing),--since REF/--changed(git-aware),--dedupe-content,--tokenizer exact(tiktoken viacarrel[tokens]),--outline;.gitignorenegation (!pattern) is honored. - Added:
carrel catalog export|import|statusandcarrel index --status; the desk database carriesPRAGMA user_versionand migrations (D-009), so tags and notes can move between machines and schemas can evolve safely. - Added: MCP server v2 — 10 tools (
carrel_search,carrel_pack,carrel_inspect,carrel_tag,carrel_note,carrel_index,carrel_convert,carrel_diff,carrel_redact,carrel_doctor) built on the same implementation functions as the CLI, pluscarrel://file/{path}andcarrel://search/{query}resources. - Added:
carrel completion bash|zsh|fish;CARREL_BIN_<NAME>=/pathpins a specific binary for an adapter (D-008, the one exception to config-free). - Added: plugins
carrel-documents(redact/sign/form/proof/color) andcarrel-guard(aPreToolUsehook that hands Claude the text of PDFs, Office files and images instead of the binary; aSessionStartcapability summary); every CLI command now has a slash command, with usage blocks generated from--helpbyscripts/sync_plugins.py --check. - Changed (breaking): Textual is an optional extra — install
carrel[tui]orcarrel[all]forcarrel desk(D-007). A plain install exits 3 with that hint. - Changed: nine adapter entries that no command used (
gs,pngquant,jq,mlr,rg,fd,sqlite3,inotifywait,claude) were removed fromdoctor;gitwas added. - Changed:
docs/REFERENCE.mdis generated from--help(scripts/sync_reference.py --checkgates drift); cookbook and snippets have a docs page; CI adds macOS (required after this release) and Windows (advisory)test-minimaljobs and an 80% coverage floor. - Known limitation:
pack --queryranks only files the index knows;carrel indexskips unknown types such as.py, so query-driven packing fits document trees, not source trees yet.
v0.1.2 — 2026-09-04¶
Housekeeping release after the repository moved to coltonbearden/carrel.
- Fixed: the v0.1.1 wheel reported
carrel 0.1.0— the release bumpedpyproject.tomlwithout bumpingproduct.json(the source of truth). Version bumps now go throughproduct.json+scripts/sync_product.py, which also syncs the marketplace/plugin manifests,CITATION.cff, and[project.urls];tests/test_product_sync.pyfails if any copy drifts. - Fixed:
--jsonis now accepted after the subcommand for every data command (carrel pack --json …), not only before it.carrel watch --jsonimplies--json-lines. - Fixed: external-tool timeouts surface as a clean error (exit 1 with the
binary name) instead of a traceback;
watchactions get--action-timeout(default 300 s) so a hung action can no longer wedge the watcher. - Fixed:
indexexits 3 (not 0) when every file was skipped for a missing binary;redact --fail-emptyprints why it exited 5. - Changed: all links, badges, and
claude plugin marketplace addtargets point atcoltonbearden/carrel; install hints useuv tool install carrel. - Changed: dependencies bumped for security advisories (pypdf ≥ 6.16.1, mkdocs-material ≥ 9.7.7).
- Fixed (review follow-ups):
--jsonalso works after nested subcommands (carrel tag ls … --json); an unchangedindexre-run stays exit 0 and--updatehook mode never fails; per-file tool timeouts are recorded instead of aborting the walk; a timed-outwatchaction is killed as a whole process group; undecodable tool output can no longer crashdoctor;sync_product.pyescapes TOML strings and touches only plugin versions; the publish workflow requires the release commit to be onmainand diffs every generated file. - Internal: ruff + mypy gate in CI, SHA-pinned actions, split build/publish
release workflow with tag↔version check and attestations, Dependabot config,
SECURITY.md,CODEOWNERS,py.typed, pre-commit.
v0.1.1 — 2026-08-12¶
First PyPI release: pip install carrel, published from CI via PyPI Trusted
Publishing (.github/workflows/publish.yml, OIDC — no token). Repo-side
discoverability round: CODE_OF_CONDUCT, issue forms + PR template,
CITATION.cff, FUNDING.yml, Related-projects README section, repo homepage +
Discussions + social preview. No CLI behavior changes.
v0.1.0 — 2026-07-16¶
Initial release: carrel CLI (24 commands), desk TUI, Claude Code plugin marketplace (5 plugins), MCP server, docs package. Built 2026-07-16.