Documentation
¶
Overview ¶
Package claudecode is the adapter boundary for everything Claude-Code-specific: hook payloads, the status-line payload, CLI invocation and version pinning.
The boundary rule (kb:spec/canary, kb:adr/process-testing-bar-e2e-always-unit-for-logic): no package above this one may know that the managed agent is Claude Code. They deal in Muster's own domain types — sessions, states, usage samples — so that a Claude Code interface change is a one-package fix, and supporting another agent CLI later means adding a sibling adapter rather than editing the daemon.
Two measured facts shape everything here. Both come from the step-1 spikes against 2.1.233 and are recorded in docs/history/spikes/canary-fields.md:
- Hook payloads carry no timestamp and no sequence number. Ordering must come from a monotonic per-session seq assigned at ingest; prompt_id and tool_use_id are the only correlation keys available.
- Hook delivery is best-effort and at-most-once. A dropped event is dropped permanently, with no retry and no replay, so every consumer must tolerate gaps rather than assume a complete event stream.
Every hook Muster registers, including SessionStart and the status line, is a type:"command" wrapper script rather than a plain HTTP hook (kb:adr/ingest-all-hooks-command-wrappers) — Claude Code's own type:"http" transport left an unmanaged session's failures visible inline and a stopped daemon noisy on every tool call. The wrapper exits 0 silently in both cases (kb:anchor/ingest.transport). Wire shapes on /ingest/* are unchanged.
theme.go also owns Claude Code's own global config file (name, location, and its "theme" key) — read-only, polled for the theme family that grounds the terminal pane and drives the dashboard's "Follow Claude Code" preference (kb:adr/theme-claude-theme-read-only-poll).
Interpret also reads a background subagent's agent marker (kb:fact/subagent-hooks-carry-agent-id) and exposes it to internal/session only as StateInput.FromSubagent — a neutral bool, never the payload key name.
Index ¶
- Constants
- Variables
- func AtomicWriteFile(path string, content []byte, perm os.FileMode) error
- func BuildArgv(binary string, p LaunchParams) []string
- func DefaultConfigPath() (string, error)
- func DefaultPlansDir(home string) string
- func Floor() string
- func FormatRange(floor, verified string) string
- func IngestHookPath(token string) string
- func IngestStatusPath(token string) string
- func InstalledVersion(ctx context.Context, bin string) (string, error)
- func IsUnderDefaultPlansDir(path string) bool
- func LaunchEnv() map[string]string
- func MergeSettings(existing []byte, cfg SettingsConfig) ([]byte, error)
- func ProjectSettingsPath(dir string) string
- func ProjectsDir() (string, error)
- func PromptInterrupted(transcriptPath, promptID string) (bool, error)
- func RangeOf(rows []ObservedVersion) (floor, verified string)
- func TranscriptPath(root, dir, claudeSessionID string) (string, error)
- func ValidPermissionMode(s string) bool
- func Verified() string
- func WriteWrapperScripts(dataDir, baseURL, ingestToken string) (hookScript, statusLineScript, legacyScript string, err error)
- type BinaryIdentity
- type Event
- type FileSignal
- type InputKind
- type Kind
- type LaunchParams
- type ModelVerdict
- type ObservedVersion
- type PastSession
- type PlanFile
- type SettingsConfig
- type StateInput
- type StatusAccount
- type StatusBucket
- type StatusContext
- type StatusModel
- type StatusUpdate
- type ThemeFamily
- type TokenReader
- type UsageReport
- type UsageWindow
- type VersionReport
- type VersionStatus
Constants ¶
const ( PermissionDefault = "default" PermissionPlan = "plan" PermissionAcceptEdits = "acceptEdits" PermissionAuto = "auto" // PermissionBypass is Claude Code's danger mode (kb:adr/launch-bypass-offered-with-danger-guardrails): // every tool call runs unprompted. Claude Code's own bypass warning blocks startup // like the trust prompt (kb:fact/bypass-acceptance-blocks-startup) — Muster never // answers it. PermissionBypass = "bypassPermissions" )
PermissionDefault, PermissionPlan, PermissionAcceptEdits and PermissionAuto are every value Claude Code's `--permission-mode` flag accepts (kb:fact/permission-mode-flag-on-wire) — Claude-Code-format vocabulary, so this package is its one owner (CLAUDE.md hard rule). internal/session's PermissionMode constants derive from these (session already imports claudecode, so that direction never cycles); internal/server validates incoming requests through whichever of the two it already has in hand.
const MusterSessionEnvVar = "MUSTER_SESSION"
MusterSessionEnvVar is the pane environment variable name every generated wrapper script reads to route a hook to its session (kb:anchor/ingest.envelope) — the one declaration internal/server's session launch (buildLaunchEnv, which sets it in the pane) and this package's generated script body (which reads it) both reference, rather than each spelling "MUSTER_SESSION" by hand.
const ScrollSpeed = "5"
ScrollSpeed is the mouse-wheel scroll rate, in lines per notch, handed to Claude Code's TUI via CLAUDE_CODE_SCROLL_SPEED. Claude Code owns the wheel — its TUI enables mouse tracking (1000/1002/1003 plus SGR 1006) whether or not tmux is in the loop (spikes/S6-scroll-bandwidth.md) — so this is the only lever Muster has over scroll distance; nothing in the dashboard or in tmux can widen it (kb:adr/surfaces-scroll-speed-via-launch-env).
Why 5: Claude Code's own default measured ~1 line per notch (9 lines over 10 notches) against 17 lines for a single PageUp on the same screen, which is the "scrolling is slow" half of issue #13. 5 is the conventional terminal wheel step of 3 rounded up toward that PageUp distance, and measured 4.9 lines/notch end to end through a Muster-launched session. Values above ~10 are unverified: a higher setting ran off the top of its 120-line transcript before the rate could be read, so proportionality is confirmed at 5 and assumed, not measured, beyond it (spikes/S6-scroll-bandwidth.md).
UNSUPPORTED INTERFACE: the variable is absent from `claude --help` and was found by reading strings out of the binary; it carries no compatibility promise (kb:fact/scroll-speed-env-present). Measured on 2.1.259, inside the verified range declared in docs/claude-code-versions.md; these numbers want re-confirming if a future canary run pushes the verified ceiling past that build. `make canary`'s static tier asserts this variable's presence (by name, read from LaunchEnv()) in the installed binary, which catches an upstream rename or removal; it does not catch a change in the variable's effect on scroll rate — that stays a manual re-measurement against spikes/S6-scroll-bandwidth.md.
Variables ¶
var ErrNoCredentials = errors.New("no claude code credentials available")
ErrNoCredentials is returned by a TokenReader when no Claude Code OAuth token is available — a fresh Mac, a logged-out Claude Code, a Keychain access prompt that can't be answered non-interactively, or (FileTokenReader) a missing/unreadable scratch file.
var ErrNoSessionID = errors.New("ingest body has no usable session_id")
ErrNoSessionID marks an ingest body that is valid JSON but carries no usable session_id. Every measured hook and status-line post carries one (canary-fields.md "common set"), so this is treated as a drop, the same as malformed JSON.
ErrUnauthorized is returned by FetchUsage when Claude Code's OAuth token was rejected (401 or 403) — internal/server maps this to the wire's "unauthorized" error kind.
var PermissionModes = []string{PermissionDefault, PermissionPlan, PermissionAcceptEdits, PermissionAuto, PermissionBypass}
PermissionModes lists every value above, in the order ValidPermissionMode's error text (internal/server/launcher.go) reports them.
Functions ¶
func AtomicWriteFile ¶ added in v0.18.5
AtomicWriteFile replaces path with content, at mode perm, by writing a temp file in path's own directory and renaming it over path (kb:adr/ingest-wrapper-scripts-replaced-atomically): a reader that already opened the old path keeps reading the complete old content, and a fresh open sees either the complete old file or the complete new one, never a truncated one — unlike a plain os.WriteFile, which truncates the target in place before writing the new bytes. Content already on disk that equals content is left untouched: no write, no rename, same inode and mtime — the common daemon-restart case, where rewriting unchanged content would otherwise still tax every reader with a torn-read window for nothing. The one implementation internal/server's writeSettings and this package's writeEnvelopeScript both call, rather than each keeping its own copy of the same temp-fsync-rename sequence.
func BuildArgv ¶
func BuildArgv(binary string, p LaunchParams) []string
BuildArgv returns the full argv (binary included) for launching `claude` with p. Every non-empty PermissionMode is sent as an explicit flag verbatim, "default" included: with no flag at all Claude Code starts in its own configured default, which the 2026-09-23 probe measured as auto on the developer's machine, not manual (kb:fact/permission-mode-no-flag-follows-configured-default) — omitting the flag for "default" no longer means manual. BuildArgv trusts the string outright rather than re-validating it against ValidPermissionMode a second time (validation, or its deliberate absence, is the caller's job, per LaunchParams' own doc): a resume-from-list passes its transcript's recorded mode through unchanged even when it names one the launch form itself never offers, such as dontAsk (kb:adr/launch-resume-passes-any-recorded-mode). An empty PermissionMode — never produced by any of this package's own callers today — omits the flag.
func DefaultConfigPath ¶
DefaultConfigPath returns the default location of Claude Code's global config file: the user's home directory joined with its name.
func DefaultPlansDir ¶ added in v0.13.0
DefaultPlansDir returns "<home>/.claude/plans" — Claude Code's plan directory absent a settings.plansDirectory override (kb:fact/plan-file-path-in-transcript).
func Floor ¶ added in v0.10.0
func Floor() string
Floor is the lowest version `make canary` has gone green on.
func FormatRange ¶ added in v0.10.0
FormatRange renders a floor/verified pair for display: "a–b" (en dash), or just "a" when they are equal (only one version has ever been observed).
func IngestHookPath ¶ added in v0.18.3
IngestHookPath and IngestStatusPath build the path (no scheme or host) for token's two ingest endpoints. internal/server uses them to register its mux routes; this package uses them to compose the full URL embedded in a generated wrapper script.
func IngestStatusPath ¶ added in v0.18.3
func InstalledVersion ¶
InstalledVersion reports the version of the claude binary at bin — the same -claude-bin value the daemon launches sessions with (a bare "claude" resolves on PATH), so a test daemon's stub answers `--version` and no test ever runs the real binary (CLAUDE.md; docs/conventions.md §Testing).
The caller supplies the context: this runs on the daemon's startup path, and a hung claude binary must not stall it indefinitely.
func IsUnderDefaultPlansDir ¶ added in v0.13.0
IsUnderDefaultPlansDir reports whether path sits under the current user's default plans directory — the reader's third scan trigger (kb:spec/reader): "a Write/Edit whose path sits under the default plans directory". Unlike LocatePlanFile this resolves the home directory itself: InterpretFiles' signature is fixed by the ingest path and has no home to thread through, so a test needing a different answer sets $HOME.
func LaunchEnv ¶
LaunchEnv returns the Claude-Code-specific environment shared by a launch and a resume. It lives here, not at the two call sites in internal/server, so no Claude Code variable name leaks outside this package (the package-boundary hard rule). Callers merge it over their own env; it never overwrites MUSTER_SESSION or the locale pair, which use different keys.
func MergeSettings ¶
func MergeSettings(existing []byte, cfg SettingsConfig) ([]byte, error)
MergeSettings deterministically merges Muster's hooks/statusLine entries into existing (the current file content, nil/empty if the file didn't exist yet), preserving every key and every hook event Muster doesn't own, and every foreign hook entry *within* an event Muster does own. Muster's own entries are replaced wholesale each call, so a token/port change heals itself (the entries reference stable script paths, not URLs — WriteWrapperScripts replaces the scripts at daemon start whenever their content, the URL or the token, changed); calling this twice with the same cfg produces byte-identical output. Every entry is a `/bin/sh -c` command line, not a path field (kb:anchor/ingest.envelope "Command fields are shell command lines"), so the configured script path is written through shellQuote — a bare space-bearing path (the default macOS data dir, `~/Library/Application Support/Muster`) would otherwise word-split and fail to execute (kb:fact/hook-commands-are-shell-lines). This writes no `type:"http"` entry on any event and no `allowedHttpHookUrls` key (kb:adr/ingest-all-hooks-command-wrappers): an already-instrumented directory's legacy http entries and Muster ingest URLs are stripped, never replaced with new ones.
func ProjectSettingsPath ¶ added in v0.18.3
ProjectSettingsPath returns dir's project-scoped settings file (kb:adr/launch-settings-local-json-not-settings-json, kb:adr/launch-project-scoped-settings-not-config-dir): the one file MergeSettings' output is ever written to. internal/server's launch path asks for this rather than spelling ".claude/settings.local.json" itself, so where Claude Code reads project-local settings lives in exactly one place (CLAUDE.md hard rule: all Claude-Code-format knowledge lives in this package).
func ProjectsDir ¶ added in v0.19.0
ProjectsDir returns the default location of Claude Code's transcript store, ~/.claude/projects (kb:fact/transcript-dir-encoding) — never under CLAUDE_CONFIG_DIR (kb:fact/config-dir-breaks-oauth applies to settings; this is a separate, read-only concern, but the same "never CLAUDE_CONFIG_DIR" rule holds). cmd/musterd's -claude-projects-dir flag overrides this default; every E2E daemon passes its own scratch directory.
func PromptInterrupted ¶ added in v0.19.3
PromptInterrupted reports whether the transcript at path records an interrupt of the prompt promptID. It reads only the file's tail (the interrupt line is the last line of its prompt, so transcriptTailBytes is enough). A missing file is (false, nil): a session that has not written a transcript yet has not been interrupted.
func RangeOf ¶ added in v0.10.0
func RangeOf(rows []ObservedVersion) (floor, verified string)
RangeOf returns rows' semver minimum and maximum version strings — the floor and the verified ceiling — independent of row order. Empty rows yields empty strings.
func TranscriptPath ¶ added in v0.19.1
TranscriptPath returns root/<encoded dir>/<claudeSessionID>.jsonl for the transcript of a session run in dir, dir being symlink-resolved first. For a directory whose encoded name exceeds maxEncodedDirNameLen the real folder carries an unidentified hash suffix, so the path returned is the unsuffixed candidate and will not exist; callers wanting an existing file for such a directory should use PastSessions. The error is reserved for an empty claudeSessionID.
func ValidPermissionMode ¶ added in v0.18.3
ValidPermissionMode reports whether s is one of PermissionModes.
func Verified ¶ added in v0.10.0
func Verified() string
Verified is the highest version `make canary` has gone green on — the ceiling.
func WriteWrapperScripts ¶
func WriteWrapperScripts(dataDir, baseURL, ingestToken string) (hookScript, statusLineScript, legacyScript string, err error)
WriteWrapperScripts generates the two command-hook wrapper scripts — the single hook wrapper serving all twelve events and the status line — into dataDir, returning their absolute paths plus the original legacy SessionStart-only script path (for SettingsConfig.LegacyCommands) whose file this also best-effort removes. Each script reads stdin, wraps it in the kb:anchor/ingest.envelope envelope from $MUSTER_SESSION/$TMUX_PANE, and POSTs it with a 2s timeout, always exiting 0.
Types ¶
type BinaryIdentity ¶ added in v0.18.5
BinaryIdentity is a resolved `claude` binary's cache key (kb:adr/launch-model-check-cached-per-binary-identity): the path exec.LookPath and EvalSymlinks resolve it to, plus that file's size and modification time. It is generic file identity, not anything about Claude Code's own versions/ layout — a Homebrew upgrade or an in-place binary replacement changes size and/or mtime either way, so both invalidate a cache keyed on this the same way. Comparable with ==, a plain value type.
func ResolveBinaryIdentity ¶ added in v0.18.5
func ResolveBinaryIdentity(bin string) (BinaryIdentity, error)
ResolveBinaryIdentity resolves bin on $PATH (following symlinks) and stats the result. A binary that can't be found, has a dangling symlink, or can't be stat'd returns an error — the caller treats that the same as a check that couldn't run: unchecked, and never cached.
type Event ¶
type Event struct {
SessionID string
Type string // hook_event_name verbatim, or "status_line"
PromptID *string
ToolUseID *string
MusterSession *int64
TmuxPane *string
Payload json.RawMessage // verbatim inner payload
}
Event is the neutral, wire-format-free result of parsing an ingest body. Every field here is Muster's own; nothing outside this package needs to know hook_event_name, session_id, or the envelope's musterSession/tmuxPane keys.
func ParseIngestBody ¶
ParseIngestBody detects the envelope shape (a "payload" key present) versus a raw hook body, and extracts the fields Muster's ingest path needs. kind decides the persisted type for a body with no hook_event_name of its own (the status-line endpoint).
type FileSignal ¶ added in v0.13.0
type FileSignal struct {
// TranscriptPath is the event's own transcript_path — every hook carries it
// (kb:fact/hook-payload-fields).
TranscriptPath string
// WrittenPath is tool_input.file_path for a PostToolUse Write, Edit or MultiEdit —
// "" for every other event (kb:fact/plan-file-path-in-transcript's "plans are
// written with Write" measurement, applied here to Edit/MultiEdit by inference).
WrittenPath string
// PlanMaybeReady is true on SessionStart, on PreToolUse/PostToolUse of
// ExitPlanMode, and on a WrittenPath under the default plans directory — the
// reader's bounded scan triggers (kb:spec/reader), never on every hook.
PlanMaybeReady bool
}
FileSignal is the neutral file-change vocabulary the reader feature (kb:spec/reader) derives from one persisted event's payload: internal/server never reads transcript_path, tool_name or tool_input.file_path directly (CLAUDE.md hard rule) — only these fields.
func InterpretFiles ¶ added in v0.13.0
func InterpretFiles(eventType string, payload []byte) FileSignal
InterpretFiles derives FileSignal for one persisted event. eventType is the event type column's value (a hook_event_name verbatim); payload is its verbatim inner JSON. This and LocatePlanFile are the only functions that read transcript_path, tool_name or tool_input.file_path.
type InputKind ¶
type InputKind string
InputKind is the neutral vocabulary the kb:anchor/state state machine (internal/session) operates on. This is the whole interface between the two packages: internal/session never reads a Claude Code payload key or event name (CLAUDE.md hard rule) — only these values. (Named InputKind, not Kind, to avoid colliding with the ingest Kind type (KindHook/KindStatus) already declared in this package by ingest.go.)
const ( KindBind InputKind = "bind" KindClearRebind InputKind = "clear_rebind" KindResumeBind InputKind = "resume_bind" KindTurnActivity InputKind = "turn_activity" KindNeedsInputPermission InputKind = "needs_input_permission" KindIdleNotice InputKind = "idle_notice" KindTurnClosed InputKind = "turn_closed" KindTurnInterrupted InputKind = "turn_interrupted" KindTurnFailed InputKind = "turn_failed" KindCompactionBegan InputKind = "compaction_began" KindCompactionEnded InputKind = "compaction_ended" KindDeathHint InputKind = "death_hint" KindClearDeathHint InputKind = "clear_death_hint" KindInert InputKind = "inert" )
type Kind ¶
type Kind string
Kind selects which ingest endpoint received the body. The status-line payload carries no hook_event_name of its own (docs/history/spikes/canary-fields.md), so the caller must say which endpoint it came in on.
type LaunchParams ¶
type LaunchParams struct {
Model string // optional; empty omits --model (a resume-from-list never names one — the transcript's own model already comes back)
Title string // optional; empty omits --name
PermissionMode string // any non-empty value is sent verbatim via --permission-mode, whether or not it's in PermissionModes (kb:adr/launch-resume-passes-any-recorded-mode); empty omits the flag
// ResumeSessionID is non-empty for a resume relaunch (kb:anchor/sessions.resume):
// emits `--resume <id>` and omits `--name` — the only place the `--resume` flag
// string may appear.
ResumeSessionID string
}
LaunchParams are the neutral inputs to building the `claude` CLI invocation (kb:anchor/sessions.create). BuildArgv assembles argv from these values as given: it never re-validates PermissionMode against PermissionModes (see BuildArgv's own doc), so validating a value here, or deliberately not, is the caller's job.
type ModelVerdict ¶ added in v0.18.1
type ModelVerdict int
ModelVerdict is CheckModel's neutral result — the caller branches on it and never sees modelCatalogSentence itself (the package-boundary hard rule).
const ( // ModelRecognised covers every outcome except the measured refusal: a known model, an // older binary with no catalog, or a check that could not run — the check fails open // (kb:adr/launch-model-check-cached-per-binary-identity). ModelRecognised ModelVerdict = iota // ModelUnrecognised means stderr carried modelCatalogSentence. ModelUnrecognised )
func CheckModel ¶ added in v0.18.1
func CheckModel(ctx context.Context, bin, dir, model string) (ModelVerdict, error)
CheckModel runs the zero-token pre-check (kb:fact/model-catalog-precheck-zero-token) for model in dir: `<bin> --bare --no-session-persistence --model <model> -p ""`, bounded by modelCheckTimeout. A run error (the binary can't start, or blocks past the deadline) is returned to the caller — the model-catalog cache (modelsFeature) is what turns it into an uncached "unchecked" verdict and lets the launch fail open (kb:adr/launch-model-check-cached-per-binary-identity); the verdict is ModelUnrecognised iff stderr carries modelCatalogSentence, never the exit code.
type ObservedVersion ¶ added in v0.10.0
ObservedVersion is one row of the observed-versions record (observed_versions.txt): a Claude Code version `make canary` has gone green on.
func ObservedVersions ¶ added in v0.10.0
func ObservedVersions() []ObservedVersion
ObservedVersions returns the embedded observed-versions record, parsed fresh on every call — a handful of lines, no package-level cache, no init() (docs/conventions.md §Go). A parse failure here is a programming error the build's own tests guard against, so it panics like regexp.MustCompile rather than threading an error through every caller.
func ParseObservedVersions ¶ added in v0.10.0
func ParseObservedVersions(r io.Reader) ([]ObservedVersion, error)
ParseObservedVersions parses the observed-versions record format (docs/claude-code-versions.md "Record file format"): one `<version> <date> <note>` row per line, `#` comments and blank lines allowed, rows need not be sorted. Rejects a duplicate version or a malformed row so a corrupt record fails loudly rather than silently narrowing the range.
type PastSession ¶ added in v0.19.0
type PastSession struct {
ClaudeSessionID string
Title *string // last custom-title, else last ai-title, else nil
LastPrompt *string // the raw last last-prompt line, untruncated
LastActiveAt time.Time
PermissionMode *string // the last permission-mode line, verbatim; nil when none recorded
Model *string // the last assistant line's model; nil when none recorded
}
PastSession is one Claude Code session found in the projects directory for a directory (kb:anchor/pastsessions.list) — internal/claudecode never writes under the projects directory, only reads it. Model is not part of the GET /api/past-sessions wire shape; it seeds a resumed-from-list session's response Model instead.
func PastSessions ¶ added in v0.19.0
func PastSessions(root, dir string) ([]PastSession, error)
PastSessions returns every Claude Code session recorded for dir (resolved for symlinks, then matched against each transcript line's own recorded cwd — kb:fact/transcript-dir-encoding's shared-folder case). root is Claude Code's projects directory (ProjectsDir's default, or -claude-projects-dir). An absent or unreadable root yields an empty list, not an error — a fresh machine's Claude Code may never have created one.
type PlanFile ¶ added in v0.13.0
type PlanFile struct {
Path string // absolute; "" when the session has never entered plan mode
// Source is "planFilePath" or "slug" — which transcript field produced Path.
Source string
}
PlanFile is where a session's plan-mode plan lives, derived from its transcript.
Claude Code (2.1.270, read out of the installed bundle) names the plan `<plansDir>/<slug>.md`, where plansDir is `settings.plansDirectory` resolved under the project root, else `~/.claude/plans`, and slug is a per-session value minted when plan mode is first entered. The transcript records both: every line written after the slug exists carries a top-level `slug`, and each plan-mode system reminder is an `{"type":"attachment","attachment":{"type":"plan_mode","planFilePath":…,"planExists":…}}` line (and a matching `plan_mode_exit`/`plan_mode_reentry` line) whose planFilePath is the resolved absolute path. planFilePath is authoritative (it honours plansDirectory); the slug is the fallback for a transcript that has the slug but no reminder line yet (kb:fact/plan-file-path-in-transcript).
func LocatePlanFile ¶ added in v0.13.0
LocatePlanFile derives a session's plan file from the transcript_path every hook carries. A missing transcript is not an error: it is the "no plan" answer. home is the resolved user home directory the slug fallback joins against — an explicit parameter rather than os.UserHomeDir() read internally, so a caller's test controls it without mutating the process-wide $HOME (docs/conventions.md § Go: tests never mutate state shared with other tests in the package).
type SettingsConfig ¶
type SettingsConfig struct {
// HookCommand is the absolute path to the generated hook wrapper script, registered
// on every event in allHookEvents — raw, unquoted; MergeSettings quotes it
// (shellQuote) at the write boundary.
HookCommand string
// StatusLineCommand is the absolute path to the generated status-line wrapper
// script — raw, unquoted; MergeSettings quotes it (shellQuote) at the write boundary.
StatusLineCommand string
// LegacyCommands lists prior wrapper script paths (bare or quoted) isMusterEntry
// must still recognise and drop from an already-instrumented directory — e.g. the
// original SessionStart-only hook-sessionstart.sh.
LegacyCommands []string
}
SettingsConfig is what MergeSettings needs to write Muster's entries into a directory's .claude/settings.local.json (kb:anchor/ingest.envelope). There is no URL and no token in this file at all — every event (including SessionStart) is a type:"command" entry pointing at a stable script path, and the ingest URL lives only inside that script (WriteWrapperScripts), per kb:adr/ingest-all-hooks-command-wrappers.
type StateInput ¶
type StateInput struct {
Kind InputKind
// PermissionMode is set only when the payload carried permission_mode (the latch's
// "source:hook" case) — nil means "this event never carries the field", never "reset
// to unknown" (kb:fact/permission-mode-presence-split).
PermissionMode *string
// Model is SessionStart's optional model id, when present.
Model *string
// FailureError is StopFailure's raw error token, displayed verbatim and never
// switched on (H2: the taxonomy isn't 1:1).
FailureError *string
// LastActivity is a closing Stop/StopFailure's last_assistant_message, verbatim
// (daemon-side truncation to 200 chars is internal/session's job, not a wire-format
// concern).
LastActivity *string
FromSubagent bool
// Agent is the subagent's opaque id when the payload carries the agent marker, "" for
// the main agent — derived for exactly the events FromSubagent is. It names who raised
// or performed the event, so the state machine can tell one subagent from another
// (kb:adr/lifecycle-attention-owned-by-raising-agent); never compared with a payload
// key, only with another Agent value.
Agent string
// AgentUnknown is true for an event that cannot say which agent it concerns — a
// Notification never carries the marker, so its empty Agent means "unknown", not
// "main". The state machine keeps the recorded owner of a permission wait rather than
// overwriting it with "".
AgentUnknown bool
// BackgroundTasks is a Stop's count of background_tasks entries still running; nil for
// every other event and for a Stop that carried no list (the count then stays as it was,
// kb:adr/lifecycle-background-tasks-count-not-state).
BackgroundTasks *int
// Prompt is a genuine user prompt's text, truncation left to internal/session
// (mirrors LastActivity): present only for a real prompt-submit turn-activity event
// whose text isn't the synthetic background-completion re-invocation
// (kb:fact/background-completion-new-prompt-id); nil for every other turn-activity
// event and whenever the field is absent.
Prompt *string
// Cwd is the working directory a main-agent hook reported (kb:fact/cwd-follows-claude-mid-session),
// nil when the event carried none, an empty one, or came from a subagent
// (kb:adr/lifecycle-claude-location-from-main-agent-cwd). Display-only: the state machine
// never reads it.
Cwd *string
}
StateInput is what Interpret derives from one persisted event's type + payload: the kb:anchor/state.transitions transition table's own vocabulary, plus the neutral fields the state machine needs to carry forward. Nothing here is a Claude Code field name.
func Interpret ¶
func Interpret(eventType string, payload []byte) StateInput
Interpret derives the neutral StateInput for one persisted event. eventType is the event.type column's value (a hook_event_name verbatim, or "status_line"); payload is its verbatim inner JSON. This is the only function in Muster that reads event-type-specific payload keys (canary-fields.md is the field inventory).
type StatusAccount ¶
type StatusAccount struct {
FiveHour StatusBucket
SevenDay StatusBucket
Model StatusModel
}
StatusAccount is the neutral account-usage reading a status-line payload carries. It is deliberately this package's own type rather than internal/usage's Sample, so the adapter stays dependency-free of the aggregator (and, transitively, the storage layer); internal/server maps it into a usage.Sample at the seam. Field meanings match usage.Sample one-for-one.
type StatusBucket ¶
StatusBucket is one neutral usage-limit readout (five-hour or seven-day) with the wire's epoch reset time already converted to UTC (kb:fact/rate-limits-wire-shape).
type StatusContext ¶
StatusContext is the neutral context-gauge triple. Its three fields all come from the same context_window object and are always adopted together, never partially.
type StatusModel ¶
StatusModel is the neutral {id, displayName} the status line's model object carries.
type StatusUpdate ¶
type StatusUpdate struct {
Title *string
Model *StatusModel
Context *StatusContext
Account *StatusAccount
// Cwd is the directory Claude reports working in (workspace.current_dir, else the
// top-level cwd); nil when neither is non-empty.
Cwd *string
}
StatusUpdate is the neutral result of interpreting one status-line payload: optional title, optional model, optional context, optional account usage sample. Every field is nil when the payload didn't carry it — callers apply only what's present, never inventing a zero value.
func InterpretStatus ¶
func InterpretStatus(payload []byte) StatusUpdate
InterpretStatus derives the neutral StatusUpdate for one status-line payload. It is a pure function of the payload bytes — it never reads the wall clock — so callers that need a sample's real timestamp (internal/usage.Aggregator.Record) stamp it themselves, mirroring how internal/store stamps its own receipt times rather than trusting a caller-supplied one.
type ThemeFamily ¶
type ThemeFamily string
ThemeFamily is Claude Code's theme family, as read from its global config (kb:adr/theme-claude-theme-read-only-poll) — the daemon only ever cares which pair (light or dark) the TUI is drawing, never the specific named theme.
const ( ThemeLight ThemeFamily = "light" ThemeDark ThemeFamily = "dark" ThemeUnknown ThemeFamily = "unknown" )
func ReadThemeFamily ¶
func ReadThemeFamily(path string) ThemeFamily
ReadThemeFamily reads path — Claude Code's global config file — and maps its "theme" key to a family. Measured 2026-09-02 against the installed binary (2.1.258): the key's own value enum is ["dark","light","light-daltonized","dark-daltonized", "light-ansi","dark-ansi"], and the binary's own family test is a "light" prefix match (its ISt() helper — `r.startsWith("light")`), which the switch below mirrors:
- file missing, unreadable, not JSON, or the value not a string -> ThemeUnknown.
- key absent -> ThemeDark (Claude Code's own default, confirmed by the binary's `resolveSetting("theme","dark")`/`vo("theme","dark")` call sites).
- value prefixed "light" -> ThemeLight; prefixed "dark" -> ThemeDark; anything else (e.g. a future "solarized") -> ThemeUnknown.
Read-only: this function never opens path for writing (kb:adr/theme-claude-theme-read-only-poll).
type TokenReader ¶
TokenReader returns the Claude Code OAuth access token. It is re-read on every call — callers must never cache a value across polls, because Claude Code refreshes the Keychain item itself on its own runs.
func FileTokenReader ¶
func FileTokenReader(path string) TokenReader
FileTokenReader reads the same {"claudeAiOauth":{"accessToken":"…"}} shape from a plain file — the -usage-token-file test seam (kb:adr/usage-keychain-token-read-only) that makes it structurally impossible for a test to fall through to the real Keychain.
func KeychainTokenReader ¶
func KeychainTokenReader(user string) TokenReader
KeychainTokenReader reads the Claude Code OAuth token from the macOS Keychain item "Claude Code-credentials" (kb:fact/oauth-token-in-keychain), read-only (kb:adr/usage-keychain-token-read-only) — musterd never writes to the Keychain and never refreshes the token itself. user is the account name `security` looks the item up under (main passes os/user.Current()'s value).
type UsageReport ¶
type UsageReport struct {
ModelScoped []UsageWindow
}
UsageReport is the neutral result of interpreting one /api/oauth/usage response. ModelScoped is nil when the response carried no weekly_scoped entries — InterpretUsageReport never distinguishes "no limits key" from "limits present but empty" from "every entry filtered out"; all three produce a nil slice, and it is the caller's job (the poller) to represent "a successful fetch happened" as a non-nil (possibly empty) list.
func FetchUsage ¶
func FetchUsage(ctx context.Context, client *http.Client, baseURL, token string) (UsageReport, error)
FetchUsage calls GET <baseURL>/api/oauth/usage with the Claude Code OAuth token (kb:fact/usage-api-oauth-shape's measured headers) and decodes the response. token is never logged here or by any caller (kb:adr/usage-keychain-token-read-only).
func InterpretUsageReport ¶
func InterpretUsageReport(body []byte) (UsageReport, error)
InterpretUsageReport is the pure decode InterpretStatus mirrors for the status line — it never does I/O. Only `kind == "weekly_scoped"` entries carrying a non-empty `scope.model.display_name` become a UsageWindow (kb:fact/usage-api-oauth-shape); a missing `limits` key, other kinds, a null/empty display_name, an unparseable resets_at on one entry, and unknown top-level keys are all ignored rather than erroring — an undocumented, evolving endpoint must not be able to break the whole poll over one unexpected entry.
type UsageWindow ¶
UsageWindow is one per-model weekly-scoped usage window read from GET /api/oauth/usage's `limits[]`. Neutral vocabulary — internal/usage.ModelWindow is this seam's other half; internal/server maps between them (mirrors how it maps StatusAccount to usage.Sample).
type VersionReport ¶ added in v0.10.0
type VersionReport struct {
Installed *string
Floor string
Verified string
Status VersionStatus
Err error
}
VersionReport is CheckVersion's result. It never carries an error to the caller in a way that blocks startup (kb:adr/connection-installed-claude-classified-never-refused): Installed is nil iff Status is StatusUnknown; Floor and Verified are always populated. Err is the underlying cause when Status is StatusUnknown — carried for logging only, never for control flow.
func CheckVersion ¶ added in v0.10.0
func CheckVersion(ctx context.Context, bin string) VersionReport
CheckVersion reports the installed Claude Code version at bin against the observed range. It never returns an error to the caller (docs/claude-code-versions.md): a missing binary, a non-zero --version exit, unparseable output or a timeout all resolve to StatusUnknown with Installed nil, never a failure that stops the daemon starting.
type VersionStatus ¶ added in v0.10.0
type VersionStatus string
VersionStatus classifies an installed Claude Code against the observed range.
const ( // StatusUnknown means installed was empty, unparseable, or the check itself failed // (missing binary, non-zero exit, timeout). StatusUnknown VersionStatus = "unknown" // StatusBelow means installed is older than Floor(). StatusBelow VersionStatus = "below" // StatusVerified means installed is within [Floor(), Verified()], inclusive — // including a version strictly between two observed rows that was never itself run // (it is inferred, not individually verified). StatusVerified VersionStatus = "verified" // StatusAbove means installed is newer than Verified(). StatusAbove VersionStatus = "above" )
func Classify ¶ added in v0.10.0
func Classify(installed string) VersionStatus
Classify classifies installed against the embedded observed-versions record.
func ClassifyAgainst ¶ added in v0.10.0
func ClassifyAgainst(installed string, rows []ObservedVersion) VersionStatus
ClassifyAgainst classifies installed against rows' range. Comparison is on the leading major.minor.patch only, ignoring any suffix.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package claudecodetest builds Claude-Code-wire-format request bodies for tests in other packages.
|
Package claudecodetest builds Claude-Code-wire-format request bodies for tests in other packages. |