Skip to content

v1.3.0

Choose a tag to compare

@gwittebolle gwittebolle released this 19 Aug 14:18
· 64 commits to main since this release

What's new

2026-08-19

feat: cards and the Totals line name their car factor

With three factor sets in circulation, a shared PNG no longer says which one produced its "car equivalent": two readers comparing cards could be comparing 142 against 200 gCO2/km without seeing it. The card now prints a short factor caption under the figure ("142 g/km, ADEME 2025", "200 g/km, world avg", "393 g/mile, EPA US"; a new tag field on the car rows of data/factors.json, kept short because the full source string overflowed the metric column), and the Totals since line prints the full source after the distance, so the social draft that quotes it names the factor too. No number changes anywhere.

2026-08-18

feat: equivalences follow the user's locale

The report compared a US-grid CO2 figure against French factors: an ADEME car, a TGV meaningless outside France, and a French beef average. A car, a kWh and a kilo of beef each differ by a factor of 2 or more between countries, so three sets now ship. A French locale keeps ADEME and SNCF, and so does an undetected one: the French set is the tool's original default, and a hook or CI environment that strips LANG should reproduce the previous behavior, not silently switch factor sets. A US locale gets published EPA factors, in miles rather than km since that is both what EPA publishes and what a US reader measures a drive in: 393 gCO2e/mile, 12.4 gCO2 per smartphone charge (EPA's marginal grid rate), and 6400 gCO2e per 150 g steak (Putman et al. 2023's cradle-to-grave 42.7 kgCO2e/kg). Everyone else, non-France francophones included, gets world averages: 200 gCO2/km by car (GFEI's 167 g/km rated global new-vehicle figure plus the ~20% EU real-world gap from OBFCM data, landing between the EU and US real-world anchors), 8.7 gCO2e per charge (EPA's 0.019 kWh at Ember's 458 gCO2e/kWh world grid intensity for 2025) and 14900 gCO2e per 150 g steak (Poore & Nemecek's 99.48 kgCO2e/kg global mean for beef herds, which carries land-use change). The Gemini prompt equivalence is unchanged everywhere. Full derivations and scope caveats in METHODOLOGY.md.

Detection reads CLAUDE_CARBON_LOCALE, then LC_ALL/LC_MESSAGES/LANG (skipping C/POSIX sentinels), then macOS AppleLocale, since hooks and GUI-launched shells often carry no LANG at all. The twelve factors live in data/factors.json under equivalences, read by both /carbon-report and the card generator through a shared detection lib, so they sit inside the marketplace-cache drift check and the CI runtime-change guard (the golden vectors stay scoped to the emission factors, which did not move). On the PNG cards the FR card always carries the ADEME factor while the EN card follows the locale down to the unit, and the Totals since line quotes the set of the exported card. Contributed by Kévin Dunglas (@dunglas).

2026-08-12

feat: add beef steak equivalence

The all-time report gains a fourth equivalence: a 150 g beef steak at 4200 gCO2e (ADEME Impact CO2 beef average, 28.0 kgCO2e/kg from Agribalyse 3.2, times the portion). A food reference reads differently than transport ones, and a typical usage total lands well under one steak, so the line prints one decimal. Report only: the card stays single-metric by design. Contributed by Kévin Dunglas (@dunglas).

2026-08-03

fix: a hook already present under a different spelling was added twice

The wiring compared hook commands as raw strings, so the same script reached settings.json twice whenever the two spellings differed. That is not hypothetical: the README's manual block writes ~/code/claude-carbon/scripts/persist-session.sh while the installer writes the expanded absolute path, and Claude Code also accepts escaped spaces (~/Claude\ OS/…). Anyone who wired it by hand and later ran the installer ended up persisting every session twice and running the rescan twice. Commands are now compared on what they resolve to (tilde expanded, escaped spaces unescaped, directory resolved with pwd -P), while the entry already in the file is left spelled exactly as the user wrote it. A third-party hook whose directory does not exist is compared literally, as before.

Found by the new install tests rather than by a user, which is the point of them.

test: the install and update wiring now has a suite

tests/run-install-tests.sh, 22 assertions covering cold start, an install predating a hook, a foreign status line and third-party hooks that must survive untouched, repeated runs, equivalent path spellings, the marketplace-cache guard, the repair update.sh performs on a local clone, and a corrupt settings.json that must be left alone rather than truncated. Each case runs under its own throwaway CLAUDE_CONFIG_DIR and nothing touches the network. Removing the spelling fix makes three of them fail, so they are not decorative.

The SessionStart bug fixed earlier today would have been caught by the first case.

ci: shellcheck on every shell script

2421 lines of bash had no linter. Warnings and errors now gate the build, style notes stay advisory. The pass fixed a cd without a guard, a trap that unquoted its temp paths and would have skipped cleanup when the server had already exited, two local x="$(…)" masking return values, and an unquoted command substitution in the release script. Two tilde matches are deliberate and carry a justified shellcheck disable.

feat: the update notice names the version

⬆ /carbon-update became ⬆ 1.1.3 /carbon-update, so a patch and a minor can be told apart before deciding to interrupt what you were doing. The three fields are read in a single jq call rather than three, since the status line runs on every turn. A flag file written by an older version, without the field, falls back to the bare command. update.sh now prints the CHANGELOG URL after updating.

chore: release script and a CI guard on the version manifests

The update notice is keyed on the version in .claude-plugin/plugin.json, so shipping to main without bumping reaches nobody: today's SessionStart fix sat behind that exact gap. Two additions make the bump mechanical rather than remembered.

scripts/release.sh patch|minor|major|X.Y.Z refuses to run off main, on a dirty tree or out of sync with the remote, checks the three manifests already agree, bumps them together (plus the lockfile), re-reads them to confirm the write landed, commits, tags, pushes, and opens the GitHub release as a draft pre-filled with the CHANGELOG lines added since the last tag. It also reports whether any runtime file actually changed, so a docs-only release that would nag users for nothing is visible before it goes out. --dry-run prints every step and writes nothing; npm publishing stays opt-in behind --npm since the tarball only carries bin/.

Exercised for real against a throwaway local origin, not just in --dry-run: the bump lands on all three manifests, the commit and both pushes go through, and a failing gh release create no longer aborts silently under set -e after the tag is already pushed. It now says what is already published, that nothing is inconsistent, and prints the command plus the kept notes file to finish by hand.

scripts/check-versions.sh runs in CI. It fails when the three manifests disagree, which always means one of npm, the marketplace or the notice is lying. It warns, without failing, when files under scripts/, hooks/, skills/, data/, install.sh or bin/ changed since the last tag while the version did not: batching commits into one release is normal, silently forgetting for six weeks is not.

fix: SessionStart hook never wired by install.sh

install.sh wrote only statusLine and the Stop hook, so every curl/npx install has been missing SessionStartsafety-rescan.sh since the beginning (git log -S SessionStart -- install.sh is empty). Two things were silently off for those users: the throttled safety rescan that catches sessions the Stop hook missed (crash, kill, hook disabled), and the update notifier, since check-update.sh is launched from safety-rescan.sh and the status line only ever reads the flag it writes. They were never told a new version existed. Marketplace installs were unaffected, hooks/hooks.json declares the hook.

The settings and slash-command wiring moves out of install.sh into scripts/configure-settings.sh, called by both install.sh and update.sh, so an install predating a hook is repaired on the next /carbon-update instead of staying half-wired. update.sh previously delegated to setup.sh under a comment claiming it refreshed settings; setup.sh only ever touched the database. The merge stays additive and idempotent (third-party SessionStart hooks and a foreign statusLine are preserved), and now writes through tmp + mv: the old truncating redirect would have destroyed the user's settings.json if jq had failed mid-write.

The README's manual-install block had the same hole, so anyone wiring it by hand reproduced the bug: it now shows both hooks and says what each one does, and points at configure-settings.sh for people who would rather not hand-edit JSON (the manual path also never installed the /carbon-* commands).

2026-08-02

chore: gitignore .claude/settings.local.json

Per-machine Claude Code permission file, was sitting untracked. Last gap flagged by a repo hygiene pass against the GitHub community checklist (health score 100%, everything else already in place).

docs: MCP servers lever in Reduce your footprint

New subsection after Compact earlier: disconnecting unused MCP servers cuts the tool schemas re-sent with every request. Deliberately unquantified (no row in the Combined impact table): the overhead is mostly cache reads, whose energy factor is the methodology's widest band, so any percentage would be false precision. Mechanism plus claude mcp list / claude mcp remove only.

docs: Related projects section in the README

Four entries before Further reading, tools rather than papers: EcoLogits, CodeCarbon, ImpactIA (link only, its CC-BY-NC-SA license keeps its data out of the factors), green-claude. ImpactIA already cites claude-carbon as an integration target, so the link closes the cross-reference.