Security
This document covers the mossbear CLI and the three packages it depends on: @mossbear/protocol,
@mossbear/eval-core and @mossbear/projections. It says what runs on your machine and when,
what leaves it, how a release is built, how you can check a release yourself, and what is not
covered yet. It covers the hosted dashboard only where the CLI talks to it.
Every claim names a command you can run or a file you can open. Paths like
packages/cli/src/hooks-settings.ts are source files. Until the CLI’s source repository is
public, the readable copy is the npm package itself. Its dist/ folder is the same files
compiled by tsc with their comments kept, so dist/hooks-settings.js is
src/hooks-settings.ts. Section 4 shows how to unpack it.
1. What runs on your machine
Installing the package runs nothing. None of the four packages declares an install script. Check
each one with npm view <package> scripts, for example npm view @mossbear/protocol scripts.
Nothing runs on its own until you run mossbear init in a repository. For Claude Code, init
writes five hook entries into that repository’s .claude/settings.json. The commands are bare
names resolved through your PATH. Their spellings come from the constants in
packages/cli/src/hooks-settings.ts, and the timeouts from packages/cli/src/commands/init.ts.
| Event | Command | Timeout | What it does |
|---|---|---|---|
PostToolUse and PostToolUseFailure | mossbear-hook --harness claude_code | 5s | Reads the hook payload Claude Code passes on stdin and appends one line to ~/.mossbear/runs/<session>.jsonl. It makes no network call and always exits 0, so it can’t fail a tool call (packages/cli/src/hook-bin.ts). |
Stop, after each assistant turn | mossbear report | 180s | Uploads the queued action metadata to your Mossbear account. If this repository subscribes to bundles, it also brings their files under .mossbear/ up to date, which can pull new versions and remove files a bundle no longer contains (never a file with unsynced local edits). mossbear report --dry-run lists what it would upload and sends nothing. |
SessionEnd | mossbear complete | 180s | Uploads what is left, marks the session finished so it can be graded, and sends back the Mossbear-managed files you edited in this checkout. --dry-run works here too. |
SessionStart (startup and resume) | mossbear update; mossbear refresh || true | 90s | Installs the newest CLI release, then refreshes the files of any bundles this repository subscribes to. |
The SessionStart entry is the one to read twice. By default it installs the newest release
of mossbear at the start of every session, on every machine where the hook runs, with no
prompt. That is how a breaking change reaches a repository whose hooks still name old
commands. init says so when it writes the entry. Your choices:
mossbear init --no-auto-updaterewrites the entry asmossbear refresh || true, with no install. Runningmossbear initwithout the flag puts the install back.mossbear init --no-session-start-hookleaves the entry out of a repository that doesn’t have one yet. It doesn’t remove an entry that is already there (packages/cli/src/commands/init.ts). To drop an existing one, delete theSessionStartentry from.claude/settings.json.mossbear doctortells you which form the repository has.
mossbear update works out which package manager owns your global install (npm, bun, pnpm or
yarn) and runs that manager’s install command. If it can’t tell, it prints the command and
installs nothing (packages/cli/src/update/install-manager.ts). It stays on the channel you
installed from: an alpha install updates to the next alpha (packageSpecForChannel in the
same file). The ; means the refresh runs even when the update fails, and the trailing
|| true means a failure here never blocks your session.
No lifecycle command overwrites a file you changed when the server’s copy also changed. It
leaves both alone and reports the conflict. mossbear refresh --docs-conflict remote takes the
server’s copy, and mossbear complete --docs-conflict local sends yours. refresh never sends
your copy, even when you ask (packages/cli/src/lifecycle/operations.ts).
For Codex, init writes the same commands, each wrapped as bash -lc '… || true'
(packages/cli/src/harness-writers.ts). For every agent it wires, init prints the file it
wrote. init also copies four skills into .claude/skills/. Skills are instructions the agent
reads, not code the CLI runs.
The CLI starts other programs in four places: git, to identify the repository and commit;
your package manager, during mossbear update; your browser, during mossbear login; and
opencode, to export an OpenCode session transcript.
Your API token lives in ~/.mossbear/config.json, written with mode 0600 inside a 0700
directory (packages/cli/src/config.ts). It can also be passed in the MOSSBEAR_TOKEN
environment variable.
2. What leaves your machine
Run mossbear what-syncs. It lists, field by field, what the hooks upload about your
sessions, what never leaves your machine, and the rules for the redacted transcript excerpts
that do. The type checker and a unit test hold that list to the upload request’s schema
(packages/protocol/src/whatSyncs.ts), so a new uploaded field can’t ship unlisted. It is not
repeated here.
What it does not list is content you move on purpose: guides and files you push or import, and
the Mossbear-managed files mossbear complete sends back. Those leave as the files themselves.
One case needs no command. If a repository’s published branch (main unless
published.branch names another) carries a published block in .mossbear/config.json, every
hook run (report, complete and refresh) uploads the files that block names, as that branch
holds them, whenever the clone’s copy of the branch has moved since it last published
(packages/cli/src/lifecycle/publish-from-branch.ts). It reads only what the clone already has
and never fetches. With no such block, nothing is published this way.
The CLI sends to and reads from your Mossbear dashboard. That is https://app.mossbear.io
unless you set --dashboard-url or MOSSBEAR_DASHBOARD_URL (packages/cli/src/config.ts).
It also asks the npm registry for the newest version (packages/cli/src/update/registry.ts):
- every time
mossbear updateruns, including from theSessionStarthook; - when you run
mossbear status,mossbear doctor,mossbear report,mossbear completeormossbear refreshyourself in a terminal. A recent answer is reused from a cache.doctorthen offers to install the update and asks before it does.
Setting MOSSBEAR_NO_UPDATE_CHECK, NO_UPDATE_NOTIFIER or CI, or "updateCheck": false in
~/.mossbear/config.json, turns off the second kind (packages/cli/src/update/notice.ts). None
of them stops mossbear update. --no-auto-update is what takes it out of the hook. The package declares no analytics or error-reporting dependency;
npm view mossbear dependencies lists what it does depend on.
3. How a release is built
- A maintainer starts every release by hand, either by pushing a
cli-v<version>git tag or by running the release workflow in GitHub Actions, which creates that tag. Nothing publishes on a schedule or on merge. - GitHub Actions builds the four packages with
tscand runs their tests. - Inside the monorepo, the packages depend on each other as
workspace:*. The workflow rewrites those to the release version: an exact version for a prerelease,^<version>for a stable one. - It packs all four tarballs, installs them together in an empty project, and runs
mossbear --versionandmossbear --helpbefore anything is published. - It runs
npm publishfor each package. A prerelease such as0.1.0-alpha.70goes out under its channel’s dist-tag (alpha). A stable version goes out underlatest. - Until a stable version exists, the workflow moves
latestto the newest prerelease, sonpm install -g mossbeargives you the newest alpha rather than the first one. - A second job installs the published version from npm, checks that
mossbear --versionmatches the tag, and runsmossbear initandmossbear doctorin an empty directory.
Two weak points:
- No provenance. The packages are published without an npm provenance attestation, because npm only accepts Sigstore provenance from a public source repository and this one is private. Nothing cryptographically links a published tarball to the commit it was built from.
- A stored token publishes. The publish step authenticates with an npm access token kept as a GitHub Actions secret, not with OIDC trusted publishing.
Publishing from a public repository would make provenance and trusted publishing possible. That has not happened yet. Until it does, read the checks in section 4 as “this is the file npm served everyone”, not “this was built from that commit”.
4. How to verify a release yourself
# Which versions the channels point atnpm view mossbear dist-tags
# The checksum and registry signatures for one versionnpm view mossbear@<version> dist
# Download the exact tarball npm serves, and read itnpm pack mossbear@<version>tar xzf mossbear-<version>.tgzless package/dist/hook-bin.js # the PostToolUse hook starts hereless package/dist/hook-logger.js # and does its work here
# Confirm the package declares no install scriptsnpm view mossbear@<version> scripts
# In a project where mossbear is installed: check registry signaturesnpm audit signaturesWhat these prove:
npm audit signatureschecks that each installed tarball carries a valid registry signature, so it is the file the registry holds. It does not prove where the file was built. It reports “verified attestations” only for packages that have provenance, and the fourmossbearpackages are not among them.npm view mossbear@<version> dist.attestationsprints nothing for them.- The unpacked
dist/is readable JavaScript, not minified, and its source maps name the source file each one came from. Reading it shows you what runs. mossbear --versiontells you which version is on yourPATH.
Comparing a tarball with the git commit it was tagged from is not possible yet, because the source repository is private.
5. What is not covered yet
- Build provenance. Section 3 explains why there is none. It needs publishing from a public repository.
- Automatic installs. The default
SessionStarthook installs whatever npm currently serves as the newest version on your channel. A bad release, or a stolen publishing token, would reach every repository with that hook at its next session start.--no-auto-update(section- takes you out of that path.
- Application-level encryption for stored content. Mossbear does not yet encrypt the guides and context you store in your account itself before writing them to its database.
- Third-party model providers. Grading and suggested guide edits run on the dashboard, and send redacted session content to third-party model providers. The privacy policy names them: https://mossbear.io/docs/privacy/
- A leaked API token. Anyone holding your token can act as you against the dashboard,
uploading to and reading from your account. If it leaks, revoke it on the dashboard’s CLI
token settings page and run
mossbear loginagain.
6. Reporting a vulnerability
Please don’t open a public issue for a security problem.
Once the CLI’s public repository exists, report through GitHub’s private vulnerability reporting on that repository. The “Report a vulnerability” button is under its Security tab. Until then there is no published security address. Mossbear is in a private beta, so reach the maintainer through the channel you were invited through, and say that it’s a security report.
There is no published response-time commitment or bug bounty.