Changelog
All notable user-facing changes to Tuff are documented on this page.
The historical entries below were reconstructed from release tags, merged pull requests, and tagged source diffs. Starting after 0.1.3, changes should be added to Unreleased when they merge and moved into a dated version section when a release is prepared.
0.16.0 - 2026-10-03
Section titled “0.16.0 - 2026-10-03”- The MCP catalog includes Screenpipe.
tuff add mcp screenpipeinstalls the officialscreenpipe-mcpserver, which searches screen text and audio that Screenpipe captured on the machine, lists meetings, and summarizes activity. It needs Screenpipe running onlocalhost:3030andSCREENPIPE_LOCAL_API_KEYfromscreenpipe auth token. TUFF_CONSOLE_DATAsets the console’s data folder.tuff console serveand thetuff console keycommands use it when--datais not given, before the$XDG_DATA_HOMEdefault.--demoignores it.
Changed
Section titled “Changed”- A demo console needs no publish credential on a non-loopback address.
tuff console serve --demo --public-read --addr 0.0.0.0:7474starts with no key and no trust. Its data is generated and in memory, and with no credential every publish gets401, so the demo is read only.--public-readis still required. - The container image sets
TUFF_CONSOLE_DATA=/datainstead of passing--data /data.docker run ghcr.io/kannandreams/tuff-console --demonow starts the demo, anddocker exec <container> tuff console key listneeds no--data. The volume stays at/data, so existing containers keep their database.
Improved
Section titled “Improved”pip install tuffcliworks on ARM64 Linux. Each release publishes amanylinux_2_39_aarch64wheel to PyPI next to the x86-64 Linux and macOS wheels.
0.15.0 - 2026-10-02
Section titled “0.15.0 - 2026-10-02”- Tuff Console has an official container image. Each release publishes
ghcr.io/kannandreams/tuff-consoleforlinux/amd64andlinux/arm64, tagged with the version, the minor version, andlatest. The image runstuff console serveas an unprivileged user with/dataas a volume. The release workflow starts it with the demo data and checks/healthzbefore publishing, and refuses to publish when it finds a fixable critical vulnerability. Each image is signed with cosign through GitHub’s OIDC identity and carries an SBOM and build provenance. Self-Hosting the Console uses the image and shows how to verify the signature, anddocker/Dockerfilebuilds the same image from the release binaries. - Releases include a Linux ARM64 binary.
tuff-aarch64-unknown-linux-gnu.tar.gzis attached to each GitHub release and listed inchecksums.txt, and the curl installer and the Homebrew formula use it on ARM64 Linux.
Improved
Section titled “Improved”- The landing page embeds a working Tuff Console preview. It runs the console’s own UI in the browser on a snapshot of
tuff console serve --demo, so a visitor can open every view, a project, and the Audit filters, and it shows no server address.mise run console-previewrebuilds it. The console UI gains apreviewsetting that hides the server line, which only this preview sets.
0.14.0 - 2026-10-01
Section titled “0.14.0 - 2026-10-01”-
tuff console serveruns Tuff Console, the self-hosted server for project reports. It listens on127.0.0.1:7474by default, or on--addr, and keeps every report in one SQLite file,console.sqlite, in--dataor in$XDG_DATA_HOME/tuff/console.POST /api/v1/reportsstores a report, and a report that equals the project’s previous one except forgeneratedAtand the project’s commit, branch, anddirtyflag adds no row and moves the latest report to the new commit, so a job that publishes on every push adds a row only when something about the project changes.GET /api/v1/projects,GET /api/v1/projects/{id}, andGET /healthzread it back. A report with aschemathe server does not read is refused with422. The database is created and migrated on first use, and a file from a newer Tuff is refused. -
tuff console key create,list, andrevokemanage API keys.createprints atuffc_key once and stores only its SHA-256. Publishing needsAuthorization: Bearer <key>whenever at least one key exists, on any address, loopback included, and a revoked key stops working at once. With no key, a loopback server accepts reports from anyone who can connect.serverefuses a non-loopback address unless--public-readis passed, because Tuff does not authenticate viewers, and unless at least one key exists. -
The
tuff-consolecrate publishes with the others. It holds the server and the store, andtuffclidepends on it. Thetuffbinary grows from 19,022,032 to 22,432,784 bytes because of the HTTP server and the bundled SQLite. -
tuff console publishsends reports to a console. It takes--server(defaulthttp://127.0.0.1:7474, orTUFF_CONSOLE_URL) and--key(orTUFF_CONSOLE_KEY), and printsstored,unchanged, orrefusedwith the console’s reason for each project. It exits with a non-zero status when the console refused any project.--all,--outdated,--project, and--dry-runwork as before. -
A GitHub Actions job publishes without a secret.
tuff console serve --trust github:<owner>accepts OIDC tokens from jobs of that owner, and--public-urlsets the audience the tokens must name. Withpermissions: id-token: writeand no key,tuff console publishasks the runner for a token whose audience is the console’s URL and sends it as the bearer. The console verifies the signature against the issuer’s published keys, the issuer, the audience, the validity window, and the repository owner. It stores a report only whenproject.repositoryequals the token’srepository, so a job can publish only as its own repository. A configured trust counts as a configured credential, so publishing authenticates on every address. -
API keys can be bound to one repository.
tuff console key create <name> --repository github.com/acme/webcreates a key that can publish reports for that repository only, andtuff console key listshows the binding. A report for another repository gets403. Existing databases are migrated to schema version 2. -
The console records audit events. Each stored report is compared with the project’s previous report, and the console records
project_first_seen,capability_added,capability_removed,version_changed,target_added,target_removed,drift_detected,drift_cleared,policy_gap_added, andpolicy_gap_closed, each with the project, the report, and the commit. A report equal to the previous one records nothing. The latest report of each project is also kept as one row per capability and target. -
Tuff Console serves a web UI and a read API. Open the server’s address in a browser for the Dashboard, Projects and project pages, Capabilities (with mixed versions and a type filter), a Harnesses matrix, Policies with every rule not enforced, an Audit log filtered by project, kind, capability, and date, and a read-only Settings view of trusts and key names. The pages are embedded in the binary, load nothing from the internet, and follow light and dark themes.
GET /api/v1/projects/{id}/reports,/capabilities,/capabilities/{type}/{id},/harnesses,/policies,/events, and/settingsback them, andGET /api/v1/projectsnow carries a summary per project. -
tuff console serve --demoserves generated sample projects. The data sits in a temporary in-memory database, is stored through the same path as a published report, and includes a monorepo, drift, outdated versions, policy gaps, and a history of reports with events of every kind. The UI marks it with a Sample data chip. -
A self-hosting guide for Tuff Console. Self-Hosting the Console covers a systemd unit with a dedicated user and
StateDirectory, a container image built from the release tarball, a reverse proxy with authentication for viewers (Caddy and nginx), keys on the host, backups of the SQLite file, and upgrades. -
CI snippets for publishing to a console. The Console page has a GitHub Actions workflow that publishes after
tuff checkon the default branch withid-token: writeand no secret, a GitLab CI job that usesTUFF_CONSOLE_KEY, and the--allform for monorepos. Validate in CI links to them.
Changed
Section titled “Changed”tuff dashboard publishis nowtuff console publish. The command from 0.12.0 keeps its flags and its--dry-runbehaviour. The old name is gone with no alias, so a script that callstuff dashboard publishmust change.
Improved
Section titled “Improved”- The landing page’s Console section shows the real console. Screenshots of the Dashboard, Audit, and Capabilities views from
tuff console serve --demoreplace the hand-drawn sample, with links to the Console docs and the self-hosting guide.
0.13.0 - 2026-09-30
Section titled “0.13.0 - 2026-09-30”tuff policy evaluateenforces policy rules an agent has no setting for. It runs as a hook before a tool call, reads the call as JSON on standard input, finds the installed policy, and answersdenyoraskin the agent’s own format with the rule and itsreason, or nothing when no rule matches. A command is split at&&,||,;,|, and newlines, a path such as/usr/bin/gitis reduced to the program name,sh -c,env,sudo,xargs,eval, and$(...)are unwrapped, and options before a subcommand are skipped, sogit -C . push --forcematches["git", "push", "--force"]. Input that is not JSON, and a policy that is no longer installed, are denied. The hook is recorded intuff.lock, sotuff checkreports a hand edit,tuff updatekeeps it, andtuff deleteremoves it.tuffmust be on thePATHwherever the agent runs.- Codex enforces
denyrules forreadandedit. They run throughtuff policy evaluateonPreToolUsein.codex/hooks.json: areadrule checks the files aBashcommand names on its command line, and aneditrule checks the files anapply_patchcall changes and the files aBashcommand writes. Codex runs project hooks only in a trusted project after/hooksapproval, and lets a call through if the hook fails. A Codex hook cannot ask, soaskrules for files stay unenforced in Codex. - Cursor enforces policies.
command,mcp, anddenyreadrules run throughtuff policy evaluateonbeforeShellExecution,beforeReadFile, andbeforeMCPExecutionin.cursor/hooks.json, registered withfailClosed: true, so a missing or failingtuffblocks the call.editrules, andaskrules forread, are not enforced in Cursor. The mapping follows Cursor’s hooks documentation and has not been checked in a running Cursor. tuff add --runtime-hookcatches reworded commands in Claude Code. It also registerstuff policy evaluateas aPreToolUsehook onBash, next to the compiled permission rules, so/usr/bin/git push --force,sh -c "git push --force", andgit -C . push --forceare refused. Checked in Claude Code 2.1.285.- A Stability page states what the 1.0 promise covers. It lists the CLI commands, flags, and exit codes, the
--jsonoutput,tuff.toml,tuff.lockschema 3, the hooks specification, and the file locations in each harness’s folder. Policies, the dashboard, and the Rust API of the published crates are excluded. The page states how a new lockfile version is read alongside older ones and how a Tuff that predates it refuses the file.
Improved
Section titled “Improved”- Docs site maintenance. The agent policies blog post shows output from tuffcli 0.12.0, and the site’s
fast-uridependency is updated for GHSA-hrr3-gc8f-f4qj.
0.12.0 - 2026-09-24
Section titled “0.12.0 - 2026-09-24”tuff dashboard publish --dry-runbuilds a project report for a dashboard server. A report holds the project’stuff.lock,tuff checkfor the project alone, and the project’s place in git: theoriginremote as one stable name (github.com/acme/agentsfor both the SSH and the HTTPS form, without credentials), the folder within the repository, the commit, the branch, and whether the folder has uncommitted changes.--allreports every folder with atuff.lockunder the current one, so each app in a monorepo is its own project.--outdatedaddstuff outdated --jsonfor the project. Outside git,--project <name>names the repository. The dashboard server that receives reports is in progress (RFC-108), so the command prints the report and sends nothing.tuff harnessand--harnessreplacetuff agentand--agent. Tuff says harness for the product it installs for, such as Claude Code or Codex, and keeps the word agent for the thing a model runs as.tuff harness list,add,remove, andset-defaultreplacetuff agent, and--harnessreplaces--agenton every command that takes it;-ais unchanged. The old names are gone, so a script that uses them must change.tuff.config.jsonalso acceptsharnessesfor its list of registered harnesses, and Tuff still writesagents. Messages and help text use the new names, so a script matchingunknown agentorregistered agentneeds the new wording. The VS Code extension passes-a, which every CLI version accepts.- MCP servers install for OpenCode through the
opencodetarget. OpenCode reads MCP servers fromopencode.json, undermcp, and not from.agents/mcp.json, so a server installed with-a open-agentsnever started in OpenCode.tuff add mcp <server> -a opencodenow writes the entry into.opencode/opencode.json, in OpenCode’s own shape:typeislocalorremote, a local server’s program and arguments are onecommandarray, its variables sit underenvironment, and a variable is referenced as{env:VAR}. The file’s other keys keep their order, since OpenCode reads the order ofpermissionrules as precedence.tuff checkreports a hand-edited entry, andtuff deleteremoves the entry, themcpobject once it is empty, and the file once only$schemais left. The docs no longer say OpenCode reads.agents/mcp.json. - Policy
mcprules are enforced in Codex.tuff addwrites them onto the server’s table in.codex/config.toml:denyadds the tool todisabled_tools, which removes it from the session, andasksetsapproval_mode = "prompt"on[mcp_servers.<server>.tools.<tool>]. Both take exact names, so a rule with*is not enforced in Codex and installs only with--accept-unenforced. The server must already be in.codex/config.toml, or the policy is refused. Codex calls aprompttool without asking when its approval policy isneverand the sandbox allows full disk access or is off, so Tuff reports these rules as enforced partially. Checked in Codex CLI 0.154.0 in a trusted project: a denied tool was absent from the session, andcodex execrefused aprompttool.tuff updateof the server keeps these settings,tuff checkignores them when it compares the server’s entry and reports them for the policy, andtuff deleterefuses to remove the server while a policy has settings on it.
Changed
Section titled “Changed”- The tagline is “Manage and govern agent capabilities.”
tuffprints “Tuff manages and governs agent capabilities.” in its welcome banner, and the crates.io, PyPI, Homebrew, and Claude Code plugin descriptions use the new tagline. A script that matched the old banner text needs the new wording.
- Codex hooks are written where Codex reads them. The Codex adapter registered hooks in
.agents/hook.jsonunder event names such aspre_tool_execution, which Codex never reads, so a hook installed for Codex never ran. It now writes.codex/hooks.json, in the grouped shape Claude Code uses, with Codex’s own events:session_starttoSessionStart,session_endtoSessionEnd,pre_tool_usetoPreToolUse,post_tool_usetoPostToolUse,stoptoStop, andbefore_finishtoStoppartially;after_savehas no Codex event and is refused. The old names stay as aliases, so a manifest that uses one still installs. Checked in Codex CLI 0.154.0, where aSessionStartand aPreToolUsehook registered this way both ran. Codex loads a project’s hooks only in a trusted project and only after they are approved with/hooks, andtuff addsays so.tuff update <id> -a codexmoves a hook installed earlier, removing its registration from.agents/hook.json. The hooks specification’s Codex matrix changes with it, without a specification version change, and the Open Agents matrix no longer cites Codex’s documentation for events no harness documents. - Codex MCP servers are written where Codex reads them. The Codex adapter registered MCP servers in
.agents/mcp.json, which Codex does not read, so a server installed with-a codexnever started in Codex. It now writes[mcp_servers.<id>]into.codex/config.toml, editing that file in place so the rest of it stays as written:commandandargsfor a local server,env_varsfor a variable forwarded under its own name,urlwithbearer_token_env_varfor anAuthorization: Bearerheader andenv_http_headersfor a plain header variable. A declaration Codex cannot carry, a renamed variable or any other header format, is refused before anything is written. Checked in Codex CLI 0.154.0, where a server declared this way was listed bycodex mcp listand reached a live session. Codex loads a project’s servers only in a trusted project, andtuff addsays so.tuff update <id> -a codexmoves a server installed earlier, removing its.agents/mcp.jsonentry unless theopen-agentstarget still uses it.
Removed
Section titled “Removed”- Workflow capabilities.
tuff create workflow,tuff add workflow, theworkflowcapability type, the[[workflow.requires]]manifest section,tuff status’s workflow dependency tree, workflow sections in the generated capability index, and the workflow checks in pack builds are gone, along with the Workflows docs page andexamples/workflows/. Workflows were marked as roadmap and are not being developed. A manifest withtype = "workflow"is refused with a message saying so, and atuff.lockthat still records a workflow is refused with a hint to remove those entries and theirworkflows/folders.tuff initno longer creates.agents/workflows/. To ship capabilities together, use a pack.
0.11.0 - 2026-09-16
Section titled “0.11.0 - 2026-09-16”tuff add --accept-unenforcedandtuff check --strictfor policies. By defaulttuff addstill refuses a policy when a selected agent does not enforce one of its rules. With--accept-unenforced, each agent gets the rules it enforces, each rule it does not is printed with the reason and recorded intuff.lock, and an agent that enforces none of the rules still refuses the policy.tuff checkprints the recorded rules on every run, lists them undergapsin--json, and exits 0 for them;tuff check --strictexits 1 while any are recorded.tuff updaterecomputes the record without the flag. Lockfiles with no recorded rules are unchanged.- Policy
commandrules are enforced in Codex.tuff addcompiles them into Codex’s own command rules in.codex/rules/tuff.rules, a file Tuff owns:denybecomesprefix_rule(..., decision = "forbidden"),askbecomesdecision = "prompt", and a rule’sreasonbecomes thejustificationCodex shows when it refuses. Checked against Codex CLI 0.154.0, which refusedgit push --forcein a trusted project. Codex loads project rules only in a trusted project and labels rules experimental. It splits a simple chain joined by&&,||,;, or|and checks each command, but checks a script with redirection, substitution, a variable assignment, a wildcard, or control flow as one command, so Tuff reports these rules as enforced partially. Where Codex never asks for approval, as incodex exec, apromptrule refuses the command.read,edit, andmcprules are not enforced in Codex, so a policy with them installs for Codex only with--accept-unenforced.tuff checkreports a rule removed from the file by hand, andtuff deleteremoves the policy’s rules and the file once none remain. - Policies are enforced in OpenCode, through a new
opencodetarget. Aftertuff agent add opencode,tuff addcompiles every kind of policy rule into OpenCode’spermissionsettings in.opencode/opencode.json. OpenCode loads that file after the project’sopencode.jsonand applies the last matching rule, and Tuff appends its rules,askbeforedeny, so the policy’s rules take precedence over the project’s own. Tuff keeps the keys, rules, and order already in the file, stops if the file has a rule for the same pattern with a different action, and does not editopencode.json. Command, read, and edit rules are enforced partially: OpenCode checks each parsed command but notsh -cor an absolute program path, and file rules cover its read and edit tools but notgrep,glob, or shell commands. MCP rules are enforced fully, and a denied tool is hidden.opencode runrejects what anaskrule raises, andopencode --autoapproves it.opencodeis a policy-only target, sotuff initdoes not register it andtuff hooks specstill lists only adapters that take hooks. The newtuff-adapter-opencodecrate publishes with the others.
Changed
Section titled “Changed”- The Claude Code plugin is now version 0.2.0. Its version had stayed at 0.1.1 since the plugin was first packaged, so Claude Code had no new version to update to, and existing installs could keep an old
tuff-cli-guideskill, including thetuff removecommand fixed in 0.10.2. Updating the plugin now brings in the current skill.
0.10.2 - 2026-09-14
Section titled “0.10.2 - 2026-09-14”- The
tuff-cli-guideskill no longer tells agents to run commands that do not exist. The guide thattuff initinstalls for each agent, and that the Claude Code plugin ships, listedtuff remove <id>, which Tuff has never had, and wrote git installs astuff add <git-url> skill <name>, which puts the URL where a local path is expected. It now liststuff deleteandtuff untrack, writes git installs type first (tuff add skill <git-url> <name>), and adds the commands it had left out:tuff scan,tuff policy matrix,tuff mcp catalogandsearch, installing a policy,tuff lock migrate, andtuff cache clear.
Changed
Section titled “Changed”- The blog has a post on agent policies across harnesses, and the blog index shows each post as a card with a link to read it.
0.10.1 - 2026-09-13
Section titled “0.10.1 - 2026-09-13”Changed
Section titled “Changed”tuff agent listnames the agents that actually read the Open Agents layout. Theopen-agentsrow listed Roo, which shut down in May 2026, and Cline, which reads skills from.cline/skills/rather than.agents/skills/. Each agent was checked against its own documentation, and the row now lists Codex, Cursor, OpenCode, GitHub Copilot, Gemini CLI, Windsurf, Amp, Goose, and JetBrains Junie. Thetuff-cli-guideskill thattuff initinstalls lists the same agents, and now also names thecodexandcursoradapters it had left out.- The documentation site is reorganised. The CLI reference is split into pages by task,
tuff.tomlandtuff.lockeach have a plain-language page under Start Here, and the landing page shows what a hook and a policy compile to for each agent.
0.10.0 - 2026-09-13
Section titled “0.10.0 - 2026-09-13”-
The hooks specification is version 0.2.0, tested by a second implementation. To find out whether the published specification was complete enough to implement, an implementation was written from it alone, by an author with no access to Tuff’s source, and a harness installed the same hook with it and with
tufffor every harness and every event name, comparing what each wrote. The two agreed on every install and refusal, every settings file, idempotency, and removal, and differed in exactly three places the specification had not described: which runtime files are installed and where, how single quotes are escaped in the wrapper, and which object the recorded hash covers. Version 0.2.0 states all three and every other point that implementation had to guess, including that a name matching no row is refused, that formatting of a settings file is not significant, and the removal algorithm step by step. It also adds three requirements to refuse hostile input: an id that is not a relative path of plain names, a listed file that escapes its directory, and a listed file that would replace the wrapper. The harness and that implementation ship as a conformance kit inspec/hooks/conformance/, andmise run checkruns it on every change, so Tuff and its specification cannot drift apart again without a failing check. -
Policies: rules that narrow what an agent may do, enforced in Claude Code. A
type = "policy"manifest lists rules with an effect,denyorask, and one subject: a command given as a prefix of its arguments, file paths the agent must not read or edit, or an MCP tool asserver:tool. Path patterns follow.gitignorerules as if the file sat at the project root. Tuff validates a policy when it is loaded and refuses a rule with no subject or two, a command argument containing a space or*, a path that is absolute or climbs out of the project, or a misspelt field. There is noalloweffect and a rule using one is refused, because a policy can come from another team’s repository or a pack, and one that could grant permissions could quietly widen what an agent may do in every project that installs it. For Claude Code,tuff addcompiles each rule into Claude Code’s own permission rules in.claude/settings.json, such asBash(git push --force *),Read(/secrets/**), andmcp__github__delete_*, merged beside your own rules and settings, and records each one:tuff checkreports a compiled rule removed by hand,tuff updatetakes out rules a changed policy no longer has, andtuff deleteremoves exactly the policy’s rules. Claude Code’s own documentation says its command and file rules are not a security boundary, since the same program run by absolute path or throughsh -cis not matched, so Tuff reports those as enforced partially and prints why at install; MCP rules are enforced fully.tuff policy matrixprints, for every agent, how each kind of rule is enforced. Cursor, Codex, and Open Agents enforce no policy rules yet, sotuff addof a policy for any of them is refused, naming each rule and the agent that would not enforce it: a policy reported as installed where nothing enforces it would be worse than none.
- A capability’s id can no longer aim an install or a delete outside the project. A capability id names the directory it installs into and the directory
tuff deleteremoves,<harness>/<kind>s/<id>, and through 0.9.0 an id was only required to be non-empty. A lockfile entry named../../../victimmadetuff deleteremove a directory outside the project, and since atuff.lockcan arrive committed in someone else’s repository, runningtuff deleteon it was enough. Every id is now required to be a relative path of plain names: nested ids such assecurity/security-reviewkeep working, while..,., empty segments, a leading or trailing/, backslashes, and NUL are refused. The rule is applied to manifest ids,--nameoverrides for local and git installs, the member ids inside a pack artifact downloaded from a registry, and every name read back from a lockfile. A lockfile holding such a name is refused as corrupt, naming the entry, rather than acted on; remove that entry by hand. - A capability’s manifest can no longer read or write files outside where it belongs. Through 0.9.0, an entry in a
tuff.tomlfileslist was joined to the capability directory to read it and to the harness directory to write it, without checking the path.files = ["../../outside.txt"]therefore copied a file from beside the capability into the project outside the harness directory, and enough../segments aimed the write anywhere the user could write. A listed file that was a symbolic link was followed, so its target’s contents were copied into the project. A native hook source added with--hook-filefollowed links the same way. Capabilities are installed from other people’s repositories, so a hostile one could have used this to overwrite files in a project or pull a local secret into it. Everyfilesentry and a tool’s entrypoint must now be a relative path inside the capability directory with no.., no leading/, and no symbolic link anywhere along it, and must name a regular file; native hook sources refuse links; and as a second line of defence nothing is written outside the install directory whatever planned it. The install is refused before anything is written. Skill directories without a manifest and capability packs already refused both and are unchanged. If you have installed capabilities from sources you do not control, review theirtuff.tomlfileslists for..or links. - A hook’s listed files can no longer replace the script Tuff runs. The registration Tuff writes runs
run.shin the hook directory, and the install note names the command it wraps. A hook that listed its ownrun.sh, orsrc/run.sh, which installs to the same place, overwrote that wrapper, so the harness ran a script the note never mentioned. That is now refused before anything is written, with a hint to rename the file. - When a hook’s event is refused, the message now lists the event names a manifest can actually use. It used to list the harness’s native names, so Cursor’s refusal suggested
preToolUsein the same message that had just refusedpreToolUse, and it namedstoptwice because two events render to it. It now reads, for Cursor,pre_tool_use (renders as preToolUse), and a native name appears as a usable name only where that harness accepts it as an alias. An event a harness knows but cannot support, such asafter_savefor Claude Code, used to be refused with only its caveat, and now lists the usable names too. - Two listed files that would install under the same path are refused. One leading
src/is removed from each listed file’s installed path, socheck.shandsrc/check.shboth install ascheck.sh. Tuff used to write both, keep whichever came second, and report the file installed twice. Such a manifest is now refused before anything is written, naming both files. The same file listed twice, with or without a leading./, still installs once. - Deleting a capability whose harness settings file is not valid JSON no longer loses its files.
tuff deleteremoved the capability’s directories first and only then read the settings file to take out its hook registrations, so a corrupt settings file made the delete fail after the files were gone, with the capability still recorded intuff.lock. It now updates the settings file first, so the same failure leaves the files in place and the capability tracked, and a retry after fixing the file completes.
0.9.0 - 2026-09-12
Section titled “0.9.0 - 2026-09-12”- The hooks specification is published. The vocabulary Tuff renders hooks from, seven canonical events with what each may block, and every harness’s compatibility matrix are now a versioned document at tuffcli.dev/spec/hooks/ and under
spec/hooks/in the repository, with a conformance checklist for anything else that wants to implement it and a JSON Schema for the machine-readable form. It is generated, not written:tuff hooks specprints the specification the binary implements, for every harness whether or not the project registers it, and--jsonprints the document the published files are made from. A repository check fails when they drift, so the published tables cannot say somethingtuff adddoes not do. The hand-kept event tables on the Hooks and Harness Adapters pages, one of which had already drifted, are replaced by links to it. The specification is version 0.1.0 and describes what Tuff has shipped since 0.1.2. tuff outdatedsays when a git install could follow a release instead of a commit. A capability added from a repository without an@follows HEAD, and until now nothing told you when that repository started tagging releases; RFC-101 had left open where that hint should live, because putting it ontuff addwould cost every untagged install agit ls-remote. It lives onoutdated, where the listing already happens: an untagged git row whose repository publishes releases gets a note on standard error under the table naming the newest release and thetuff update <id>@^<major>.<minor>that pins it, once per capability however many agents it is installed for.--jsoncarries the newest release aslatest_releaseon every git row. The note is on standard error in both modes, so the JSON stays clean.
Changed
Section titled “Changed”- The four harness adapters now share one implementation of hook-settings handling, in
tuff-core. Each adapter used to carry its own copy of the code that merges a hook registration into the harness’s settings file and takes it out again, so a fix had to be made four times at once and the copies had drifted in wording. An adapter now declares only what differs: the shape of its settings file, its paths, its event matrix, and how to recognise a project that uses it. Nothing the adapters write has changed; the only visible difference is that the messages for a malformed--hook-filefragment read the same for every harness. tuff outdatedchecks a commit-following git install with onegit ls-remoteinstead of a clone. The listing names HEAD and every tag in one round trip, so ashaentry never clones, and adeclaredentry clones only when HEAD has moved, which is when the version it declares now has to be read. Each capability is also checked once rather than once per agent it is installed for, which previously cloned a capability installed for two agents twice.
0.8.0 - 2026-09-09
Section titled “0.8.0 - 2026-09-09”Changed
Section titled “Changed”tuff.lockis JSON, lockfile schema version 3. The file keeps its name and its rows keep their fields, names, and order; only the syntax changes, from TOML to JSON in the layoutJSON.stringify(value, null, 2)produces: two-space indentation, one array element per line, a trailing newline. The reason is formatters. Repositories run pre-commit hooks and editor formatters over their JSON files, and a.lockthat is not JSON was rewritten into something Tuff could not read; asking every project to exclude the file was the wrong fix. The chosen layout is the one npm’spackage-lock.jsonuses and the onejq, Python, VS Code’s formatter, and Prettier’sjson-stringifyparser all emit, so those leave the file byte for byte unchanged; a test proves the golden fixture is a fixed point of both the writer and a formatter. Version 1 and version 2 files are read transparently by every command; read-only commands never rewrite them, the first mutating command writes version 3, andtuff lock migratedoes only the rewrite. A file whose syntax does not match its version, and a lockfile from a newer Tuff, are refused with a message saying so rather than a parse error. Tuff 0.7 and earlier cannot read a version 3 lockfile, so a project that migrates needs everyone on 0.8 or newer, the VS Code extension included; the extension itself reads the lockfile only through the CLI and needs no change. Absent optional fields on an MCP server entry, such asurlfor a stdio server, are omitted rather than written asnull; the installedserver.tomlrecords are unchanged because TOML already omitted them.- The VS Code extension shipped 0.2.0 through 0.2.3 in this window: a Get Started walkthrough, Add from Git URL, and the Marketplace listing with its recording. The extension versions independently and keeps its own changelog under
editors/vscode; nothing in the CLI changed for it. - The documentation site’s dependencies moved past four npm advisories against astro, sharp, svgo, and js-yaml. Site only; nothing in the CLI changed.
0.7.0 - 2026-09-06
Section titled “0.7.0 - 2026-09-06”tuff scanfinds capabilities Tuff is not tracking. Every inventory command reads the lockfile, so a skill written by hand in.claude/skills/was invisible tolist,status, andcheckno matter how long it had been there;tuff add <path>could adopt one in place, which made discovery, not adoption, the missing half.tuff scanreads.claude,.cursor, and.agents, and reports every capability directory in them with its id, kind, declared version, harness, and whether Tuff already tracks it.--adopttracks them, either all at once or the paths you name, and each one goes through the same code pathtuff adduses, so scanning can never track something adding would refuse. Adoption is in place: nothing is moved, copied, or rewritten. Two directories declaring the same id are reported as a conflict rather than one being adopted silently, and a directory missing something Tuff needs — a[hook]section, a[server]section — is reported with the reason instead of failing halfway through the adopt. Scanning itself changes nothing and works beforetuff inithas run;--adoptwrites to the lockfile, so it asks you to init first.--jsoncarries the same rows with the reason and aninitializedflag.tuff mcp cataloglists the built-in catalog.tuff mcp searchreaches the registry andtuff add mcp <id>installs by name, but until now nothing could show what the ids are, so finding one meant reading the website or the source. The command prints every entry compiled into your binary with its version, transport, and the environment variables it expects you to export, and--jsonadds the full invocation the harness would run and the tools the entry advertises. Every row is resolved through the same code pathtuff add mcpuses, so the listing can never offer a server the installer would refuse. It reaches no network and needs no project.- Tuff ships a VS Code extension, in
editors/vscode. It puts the capabilities installed in a project into the editor sidebar with their versions, the agents they were installed for, and whether they have drifted, and runsdiff,update,check, andmcp doctoron a row. Like the Claude Code plugin it carries no binary and runs thetuffon your PATH, and it needs 0.6.0 or newer for the--jsonoutput that release added. Cursor installs VS Code extensions, so this covers the harnesses that have no plugin surface of their own. The extension versions independently of the CLI and keeps its own changelog. - The site lists the built-in MCP catalog at /mcp-catalog/: every server’s install command, the variables it needs, the tools it answers with, and the command it actually runs, searchable and filterable by whether a key is needed and by transport. It is a standalone browse page rather than a documentation page, so the card grid gets the full page width instead of a column between two sidebars, and it carries the site navigation back into the docs. The listing is generated from
crates/tuff-core/assets/mcp-catalog.tomlon every build, the same arrangement the changelog page uses, so it cannot promise a server the CLI does not have, and the generator fails the build on an entry that would not resolve. The hand-maintained table on the MCP Servers page is replaced by a link to it, leaving one source of truth.
Changed
Section titled “Changed”- Reworded the built-in catalog’s
everythingandplaywrightdescriptions. Both used a double hyphen as a dash and one wrapped a command in backticks, which read as markup wherever the description is shown: the catalog page,tuff list, and the trackedserver.toml. The entry versions are unchanged, so no installed server reports itself outdated over wording.
- A capability already sitting in
.cursor/is now adopted where it is, for Cursor..claudeand.agentswere recognised as harness layouts and.cursorwas not, sotuff add .cursor/skills/<name>copied the files into the Open Agents layout and recorded the wrong harness. Reading the harness from the path also no longer depends on the capability being exactly one level deep, so a skill inside a grouping directory, such as.claude/skills/security/security-review/, is attributed to Claude rather than to Open Agents.
0.6.0 - 2026-09-05
Section titled “0.6.0 - 2026-09-05”- Git-sourced capabilities can be installed at a release.
tuff add skill <repo> <name>@1.2.0installs exactly that release, and<name>@^1.2the newest release in the range. Releases are read from the repository’s tags without cloning:v1.4.0or1.4.0for the whole repository,<name>/v1.4.0or<name>-v1.4.0for one capability in a monorepo, and capability-scoped tags take precedence whenever any exist, so a repository-wide tag is never mistaken for a release of one skill inside it. The chosen tag is then cloned shallowly. A requirement nothing satisfies fails before anything is cloned and lists the releases that exist; a repository with no release tags says how to tag one. The lockfile keeps pinning the commit and records the tag and the requirement beside it, with the entry’s version now the release’s andversion_scheme = "semver", using the fields lockfile v2 reserved for this. Installing without@is unchanged. - The lifecycle verbs understand the pin.
tuff updatemoves a release-pinned capability to the newest release its requirement allows, never to the latest commit, and says when that release is already installed.tuff update <id>@<requirement>records a new requirement and moves, which is how an exact pin is lifted, and--checkpreviews the release and the claimed size of the change, as into 1.4.0 (minor).tuff outdatedcompares against the newest release the repository has, with onels-remoteand no clone, and showsoutdated (minor); in--jsonthe size is a separatechangekey.tuff diff <id> --upstreamcompares against the newest allowed release rather than the latest commit, the same contentupdatewould install, andtuff diff <id>@<requirement> --upstreampreviews a different requirement beforeupdateapplies it; a note on standard error names the release and the JSON carries it asupstream. - A release tag that moved or vanished upstream is reported rather than trusted, as a pack’s registry tag already was.
tuff outdatedverifies the installed tag in the samels-remote, comparing the commit it names now with the one recorded at install: a mismatch readsrepointedand a deleted tagtag missing, both winning overoutdatedwhileLATESTstill shows the newest release.tuff updateon a repointed entry refuses to call it up to date, previews the replacement with--check, and replaces the install with--force. The lockfile pins the commit, so the install itself was never affected; what changed is what the version claims to be. - A git install that no release tag chose records the version the source declares for itself:
versionintuff.toml, elseversion:ormetadata.version:in theSKILL.mdfrontmatter, where the Agent Skills specification puts it. The lockfile marks itversion_scheme = "declared"; a source declaring nothing still records the commit SHA withversion_scheme = "sha". A declared version may not move when the content does, sotuff listandtuff outdatedshow it as1.2.0 (declared)for a git install,outdatedcompares the version declared then with the one declared now and names the claimed size of the change, and a commit that moved without a version bump still readsoutdated. A local skill without atuff.tomlalso takes its frontmatter version instead of0.1.0. tuff list --jsonandtuff outdated --jsonprint their rows as JSON arrays, andtuff diff <id> --jsonis a shorthand for--format json. The keystype,target, andstatusare spelled as intuff check --json, so a script or an editor integration reads every inventory command the same way. Rows carryversion_scheme; where theoutdatedtable shows—, the JSON carriesnull; status strings are emitted plain, without the terminal colouring the tables use.linearandcontext7join the built-in MCP catalog. Both are remote servers that authenticate with anAuthorization: Bearerheader, which the catalog could not express until[server.headers]existed; each entry now declares the header as a reference toLINEAR_API_KEYorCONTEXT7_API_KEY, andtuff add mcp linearwrites the right dialect for every selected harness with the key still in your environment. Linear’s interactive OAuth flow is not used, because a config file can carry a variable reference and not a login. Context7’s key is recommended rather than required by the vendor, but the catalog has no optional headers, so the entry asks for one; the keyless stdio form remains available from the registry asio.github.upstash/context7.
- A git install with a
tuff.tomlrecorded the commit SHA over the manifest’s own version, andtuff updateon such a source synthesized a skill manifest instead of reading thetuff.tomlasadddoes, so a tool or workflow from git would have updated as a skill. Both paths now share one helper.
0.5.0 - 2026-09-03
Section titled “0.5.0 - 2026-09-03”- The
tuff-cli-guideskill now reaches the harness you actually run.tuff initrecorded it againstopen-agentsand nothing else, and Claude Code reads.claude/while Cursor reads.cursor/, so the reference that teaches an agent to drive Tuff was invisible in the session where it was needed.initnow detects the harnesses a project already contains and emits the guide into each one’s layout: a.claude/directory or aCLAUDE.mdfile registersclaude, a.cursor/directory registerscursor. Codex is deliberately not detected, because it writes the same.agents/rootopen-agentsalready covers and its detector matches the directoryinititself creates. - A capability that is already installed can now be emitted for a harness it was not installed for.
tuff add .agents/skills/release-checklist --agent clauderecords the new target and writes the harness-native output, where it previously refused withalready in the 'open-agents' agent layout. Nothing else could do it either:tuff agent addregistered a harness without backfilling, andtuff update -a claudereported the capability was not installed for that agent. The recorded source, version, and description are preserved, so adding a harness to a capability installed from Git keeps its repository and resolved revision. A target that is already recorded is still refused, since re-emitting one is whattuff updateis for. - Tuff is installable as a Claude Code plugin, which is the same guide reaching a session before any project has adopted Tuff.
claude plugin marketplace add kannandreams/tufffollowed byclaude plugin install tuff@tuffinstalls it machine-wide, and--scope projectrecords it for everyone working in the repository. The plugin carries no binary: it expectstuffon PATH and tells the agent how to install one when the command is missing.
Changed
Section titled “Changed”- The
tuff-cli-guideskill no longer opens by asserting that Tuff is installed in the current project, which was untrue wherever the guide arrived beforetuff initdid. It now tells the agent to checktuff --version, to ask before installing anything, and to install withuv tool install tuffcli, naming Homebrew and Cargo as the fallbacks. The curl installer is deliberately absent from the agent-facing guide: it targets/usr/local/binand escalates withsudo, which stalls an agent waiting on a password nobody is there to type. Barepip installis named only as a thing to avoid, since most systems refuse it as an externally-managed environment and a virtual environment install is not on PATH afterwards.
0.4.0 - 2026-09-03
Section titled “0.4.0 - 2026-09-03”- Added
tuff mcp search <query>, which searches the official MCP registry, and taughttuff add mcp <name>to install from it when the name is not a built-in catalog id. The catalog’s twelve curated entries stay the shortcut; the registry’s thousands are now reachable by name. Tuff assembles the launch command from the entry’s package type (npmundernpx,pypiunderuvx,ociunderdocker,nugetunderdnx), pins the version the registry lists, and records environment variables as references, never values. An entry it cannot express exactly is refused with the reason rather than installed approximately.tuff outdatedandtuff updatere-resolve a registry install against its registry, and--registrypoints any of it at a self-hosted one. - MCP servers reached over HTTP can now declare the auth header they need, which is what most remote servers require and what Tuff previously had no way to express.
[server.headers]takes the same{ from_env = "NAME" }references[server.env]already takes, plus an optionalformat = "Bearer {}"for the common case where the header wraps the token, so a manifest still has no field a literal secret can occupy. Each harness gets its own dialect: Claude Code, Codex, and Open Agents expand${VAR}, Cursor${env:VAR}.tuff checkcatches a header edited by hand, and the post-install reminder names header variables alongside environment ones. tuff mcp doctornow probes HTTP servers instead of reportingunsupported transport, doing the sameinitialize,notifications/initialized,tools/listhandshake it does over stdio and reporting the real tool count. It accepts either response shape a server may choose, a plain JSON body or an SSE stream, carries the session id the server issues on initialize, and echoes the protocol version the server negotiated. Two statuses are new:unauthorizedwhen the server answers 401 or 403, kept separate fromprotocol errorbecause the fix is to check the token rather than the config, andunreachablefor a DNS, TLS, or connection failure. A variable a header references but your shell does not export is reported asmissing envbefore any request leaves the machine. Header values are read from the environment at the moment of the request, so doctor checks exactly what the harness will send; there is deliberately no--headerflag.- Remote servers in the MCP registry that authenticate with a header now install instead of being refused. A required header the entry documents as
Bearer {vendor_api_key}becomesAuthorization = { from_env = "VENDOR_API_KEY", format = "Bearer {}" }; a header the entry names without saying how to build its value becomes a reference to a variable holding the whole value, prefix included, because guessing aBearernobody wrote down would be right often and wrong silently. Optional headers are left out and named at install time, since requiring a variable the server does not require would report a working server asmissing env. A header the entry documents as a literal, such asAccept: application/json, is still refused: a manifest has no field a literal value can occupy.
- Registry entries offering only the superseded
ssetransport were installed as though they spoke Streamable HTTP, writing a config no harness could use. They are now refused with that reason, and an entry publishing both transports installs thestreamable-httpone rather than whichever was listed first. - Cursor was written a
"type": "http"key on remote MCP server entries, which its config format does not use; it distinguishes a remote server from a stdio one byurlversuscommand. Tuff no longer emits it for Cursor. Any HTTP server already installed for Cursor will show asmodifiedon the nexttuff checkuntil it is reinstalled.
0.3.0 - 2026-09-02
Section titled “0.3.0 - 2026-09-02”- Every command now reports failures by kind, so the exit code and the
--jsonenvelope say what kind of problem it is: a mistyped flag or argument exits2, while a missing capability, a refused overwrite, local changes, an unreachable source, an unreadable file, and an unsupported request all exit1with a distinctkind. Advice that used to be appended to a message with a semicolon, such asrun 'tuff agent list'oruse --force, now prints on its ownhint:line. - Pack commands now report failures by kind: an artifact that will not parse reads as corrupt, a refused overwrite as refused, local changes as drift, an unreachable registry as a source failure, and a mistyped flag exits 2. Advice that used to be appended to a message with a semicolon now prints on its own
hint:line. - Errors now carry a kind, and commands use it to choose an exit code:
0success,1a failed operation,2a command called wrongly,70a bug in Tuff. Messages that suggested a next step now print it as a separatehint:line, and a--jsoninvocation reports failures as one JSON line on stderr withkind,message, andhintfields rather than prose.
- Fixed
list,status,outdated, andcheckreporting a corrupt or unreadabletuff.lockas though nothing were installed. They now fail and say the lockfile could not be read. A global lockfile that simply does not exist is still not an error.
0.2.0 - 2026-09-02
Section titled “0.2.0 - 2026-09-02”Changed
Section titled “Changed”- Lockfile schema version 2. A capability’s origin is now one
[capabilities.source]table with akindoflocal,git,catalog, orpack, replacing thesource,repository,source_path, andresolved_refcolumns and the optionalpacktable. Every row gainsversion_scheme(declared,sha, orsemver), reserved so release-tag resolution can land later without another schema change.emittedFilesandscope, which were never persisted, are removed. Version 1 files written by 0.1.x are read transparently by every command; read-only commands never rewrite them, the first mutating command writes version 2, andtuff lock migratedoes only the rewrite. A lockfile from a newer Tuff is refused by version number rather than failing as a parse error. Version 1 stays readable throughout 0.2.x.
- Added
tuff lock migrate, which rewritestuff.lockin the current schema and changes nothing else. - Added pack updates:
tuff update <member>on a capability installed bytuff add packnow moves the whole pack forward. With a registry on record it resolves the newest semver tag, pulls it, and applies it;--pack <artifact>applies a pulled file instead, for offline use or a pack installed without--reference. Members the new release drops are removed, new members are installed, shared hook and MCP registrations follow, and--checkpreviews all of it. Local edits block the update unless--forceis given.tuff updategained--plain-httpand--ca-file, matchingtuff outdated. - Added detection of a pack tag silently repointed to different content.
tuff outdatednow resolves each installed pack’s tag and compares its digest with the one recorded at install; a mismatch readsrepointedand a deleted tag readstag missing, both taking precedence overoutdated.tuff updateon such a pack refuses to report it as up to date and explains that--forcereplaces the installed release with what the tag serves now. One manifest fetch per pack per run, no artifact download; registry lookups are also no longer repeated for every member and harness of the same pack.
- Fixed a project-scoped install landing in the global lockfile when
XDG_STATE_HOMEwas set on a machine that had used--global. The lockfile path was inferred from the directory and treated the project root as a home directory; the scope is now passed explicitly everywhere. - Fixed
tuff add packrefusing to install into any project that already had a tool, workflow, or MCP server. The generated capability index those give a project is tracked, but the pack install treated the staged copy of it as an untracked file it must not overwrite.
Improved
Section titled “Improved”- The documentation site now renders
CHANGELOG.mdas a changelog page, generated at build time so there is one copy that cannot drift, and the release checklist lives in CONTRIBUTING.md. - Rewrote the MCP Servers reference page: explained that the built-in catalog is a list of launch declarations embedded in the binary rather than server code, and replaced the manifest example’s archived npm package with the catalog’s verified Docker entry.
- Updated the documentation site’s build dependencies for four advisories published against
fast-uri; nothing in Tuff itself uses the package. - Added a blog to the documentation site at
/blog/, linked from the landing page and the docs header, with a first post walking through the MCP server capability end to end on the catalog’severythingserver. Landing-page navigation links now highlight as a dark panel on hover.
0.1.8 - 2026-09-01
Section titled “0.1.8 - 2026-09-01”- Added
mcp-serveras a capability type. One[server]declaration intuff.tomlbecomes the correctmcpServersentry in every selected harness’s config, in that harness’s dialect, plus a trackedserver.toml, solist,check,diff,update,delete, andoutdatedall work on it unchanged. Secrets are references only:[server.env]accepts{ from_env = "NAME" }and rejects a literal value at parse time. An existingmcpServersentry that Tuff does not track is refused before any file is written. - Added
tuff add mcp <id>...with a built-in catalog of 12 verified servers:filesystem,memory,github,fetch,git,time,sequentialthinking,everything,brave-search,notion,playwright, andsentry. Catalog installs recordsource = "catalog"and re-resolve against the embedded catalog onupdateandoutdatedinstead of cloning. At a terminal,tuff add mcpasks once per required environment variable whether to use a different variable name than the catalog default;--yesor a non-terminal stdin skips the prompt. - Added
tuff mcp doctor, which spawns each installed MCP server, completes theinitializehandshake, and lists its tools, so a mistyped command, a missing package, or an unset token is reported instead of failing silently inside the harness. Supports--agent,--global,--json,--timeout, and--ignore-failures, and exits non-zero on any unhealthy server. Stdio transport only;httpreportsunsupported transport. - Added drift detection for managed MCP config entries. Registering a server (or an MCP-native tool) records a baseline hash of its
mcpServersentry, sotuff checkfails on a hand-edited or removed entry,tuff listshows it as modified,tuff deleterequires--force, andtuff update --forcerestores a tampered catalog entry. Entries installed before this release are unchecked until reinstalled. - Added a generated per-harness capability index: a
tuff-capabilitiesskill listing every installed tool, workflow, and MCP server with its exact invocation. It is regenerated on every install, update, and delete, includingtuff add pack, and removed once a harness has nothing left to list. - Added
implementation,parameters,workflow, andserverfields to capability lock entries, cached at install time so the index andupdatecan see a capability’s shape after its manifest is gone. Existing lockfiles parse unchanged.
- Fixed
tuff add packstaging installs in a temporary directory that had notuff.config.json, so any per-harness step there silently saw zero configured agents. - Fixed the wire framing in the
mcp-server-toolexample server, which used LSP-styleContent-Lengthheaders instead of the newline-delimited JSON-RPC that real MCP servers speak. - Fixed the lockfile writer hardcoding
source = "git"; it now persists the recorded source type.
Improved
Section titled “Improved”- Refreshed the landing page: two-column hero with the terminal demo beside the copy, a
brewinstall tab, a strip of supported harnesses, and fixes for desktop horizontal overflow, a squeezed mobile capability grid, unstyled footer links, and a dark band under the footer.
0.1.7 - 2026-08-30
Section titled “0.1.7 - 2026-08-30”- Added
--referencetotuff add pack, recording the OCI reference a pack was pulled from sotuff outdatedcan check the registry for a newer version.tuff outdatedgained--plain-httpand--ca-file, matchingtuff pack push/pull, for checking a self-hosted registry. - Added a CI cache for Rust dependencies (
Swatinem/rust-cache).Tuff Checkdropped from about 8.5 minutes to under 2 on a warm cache; nothing else changed. - Added
gitleaksas a pre-commit hook, scanning staged changes for generic secrets before a commit exists.
- Stopped
tuff outdatedfrom reportingup to datefor a capability it had not checked — anything installed from a pack, or from a local path. It now reportsnot checked, styled to make clear it is not a clean bill of health.
Improved
Section titled “Improved”- Added credential file patterns to
.gitignore(keys, certificates, dotenv files) as a preventative measure; no leak was found. - Defined “capability pack” and “Tuff pack” once, on the page that owns the concept, and used each consistently: the vendor name where the artifact is being distinguished from a container image, the category name everywhere else.
0.1.6 - 2026-08-29
Section titled “0.1.6 - 2026-08-29”- Stopped
tuff addfrom registering a hook twice when the same capability or pack is installed over an existing install. Every adapter appended hook groups to the harness settings file unconditionally, so each re-add left another identical entry behind and the harness ran the hook once per copy. Affects the Claude, Codex, Cursor, and Open Agents adapters.
0.1.5 - 2026-08-27
Section titled “0.1.5 - 2026-08-27”- Added
tuff pack build --name <name>for packaging accepted project-scoped capabilities directly fromtuff.lock, with capability selectors, workflow dependency expansion, version and agent overrides, and atuff-dist/default output. - Added
tuff pack init <name> --from-projectfor reusable ID-based definitions undertuff-packs/without copying capability sources.
Improved
Section titled “Improved”- Made project pack builds verify selected installed files and reconstructed sources against accepted lockfile baselines before writing an artifact, with actionable
tuff updateguidance.
- Deduplicated Git capability discovery paths so a directly selected nested capability is not reported as ambiguous.
- Kept project pack builds read-only for
tuff.config.jsonwhen the default-agent configuration is absent.
0.1.4 - 2026-08-27
Section titled “0.1.4 - 2026-08-27”Improved
Section titled “Improved”- Made the “Explore Tuff Packs” landing-page link a primary call to action.
- Standardized public pack documentation around the
crm-integrationexample and linked the beginner-focused Tuff Pack examples repository from the CLI and capability-pack documentation.
0.1.3 - 2026-08-25
Section titled “0.1.3 - 2026-08-25”- Added capability packs as deterministic, versioned bundles of skills, tools, hooks, and workflows.
- Added
tuff pack init,check,build,inspect, andverifyfor authoring and validating.tuffpackartifacts. - Added atomic project installation with
tuff add pack, including per-capability pack provenance intuff.lock. - Added
tuff pack pushandpullfor OCI-compatible registries, with tag and digest references, Docker and Podman credential discovery, private CA support, and explicit opt-in plain HTTP for local registries. - Added
tuff pack extractfor producing a verified harness-native runtime tree without creating project lockfile state. - Added an Amazon ECR and Docker BuildKit guide showing digest-pinned publication, pull, extraction, and container-image delivery.
Improved
Section titled “Improved”- Made pack builds reproducible through canonical ordering and deterministic metadata, allowing identical inputs to produce identical artifact digests.
- Added safe OCI tag behavior: identical pushes are idempotent, while moving an existing tag requires an explicit
--force. - Enforced Conventional Commit subjects locally and in pull requests.
0.1.2 - 2026-08-21
Section titled “0.1.2 - 2026-08-21”Improved
Section titled “Improved”- Added canonical hook event definitions and aliases shared by adapters, with canonical names taking precedence.
- Made terminal color output respect TTY detection and improved global and name-filtered validation behavior.
- Hardened release creation so missing checksum assets fail the workflow.
- Updated Claude hook rendering to use the documented native events:
SessionStart,SessionEnd,PreToolUse,PostToolUse, andStop. - Corrected Cursor stop-event resolution.
- Prevented malformed MCP configuration from modifying files or partially installing a capability.
- Made hook shell wrappers and workflow TOML serialization safe for special characters.
- Corrected the package version after the
v0.1.1tag shipped GitHub archives whose binaries still reported0.1.0.
0.1.1 - 2026-08-20
Section titled “0.1.1 - 2026-08-20”Distribution note: this tag produced GitHub archives, but the Cargo package version was not bumped. Those binaries report
0.1.0, the PyPI workflow failed, and notuffcli==0.1.1package exists. Use0.1.2or later.
Improved
Section titled “Improved”- Adopted mise with pinned Rust, Node.js, Python, Perl, pre-commit, and documentation tooling for reproducible development.
- Hardened website dependency installation and upgraded Astro and Starlight.
- Separated the user-facing README from repository guidance and added clearer contribution, conduct, and agent-maintainer documentation.
- Added pre-commit branch-name validation and improved installation-script behavior.
- Corrected documentation table styling and ensured
rustfmtand Clippy are installed with the pinned Rust toolchain. - Made GitHub release creation fail when expected checksum assets are missing.
0.1.0 - 2026-07-26
Section titled “0.1.0 - 2026-07-26”- Released the Rust-based
tuffCLI for managing project-owned agent skills, tools, hooks, and workflows. - Added local and Git-backed capability installation, project and global scopes, and adapters for Open Agents, Claude, Codex, and Cursor.
- Added
tuff.locklifecycle tracking with cached baselines, drift detection, upstream comparison, diff, update, validation, delete, and untrack workflows. - Added capability index and project report generation, hook portability checks, and agent registration and default selection.
- Added GitHub release archives for macOS arm64, macOS x86_64, and Linux x86_64, plus installation through PyPI, crates.io, Homebrew, and the install script.
- Added the Astro and Starlight documentation site and the initial Tuff landing page.
Improved
Section titled “Improved”- Renamed the project from Coral to Tuff and standardized the CLI, manifests, documentation, and adapter terminology.
- Refined adapter and renderer contracts so harness-specific output remains isolated behind dedicated adapter crates.
- Added repository validation, integration tests, release automation, and reproducible Cargo builds.