Measures the start of a Claude Code session by source, and the usage of each run: model calls, context that each call reads again, and skills that a period did…

dotclaude changes Claude Code through its official extension points: settings, hooks, skills, agents, and an output style. It does not patch the Claude Code binary. Its goal is the highest quality for the lowest token cost.
The wiki explains each part and the reason for it. Start with the Home page, or read the Install page for the full setup.
dotclaude is a marketplace with one core plugin and seven add-on plugins. Each add-on works without the core plugin.
| Plugin | What it gives |
|---|---|
dotclaude | The core: the settings snippet, the status line, the output style, six agents, ten skills, the guard and asks hooks, the spec store, and the second_opinion tool. |
dotclaude-codegraph | The code graph of a project, from the CLI codegraph. |
dotclaude-browser | The browse skill, and a headless browser search for each WebSearch call. |
dotclaude-docs | The docs skill, with current library docs from the CLI ctx7. |
dotclaude-sessions | The start-cost and usage skills, which measure the session start and the usage. |
dotclaude-lab | Tools for plugin authors: the sandbox, capture, schema, and eval compare. |
dcc-plugin-agents-md | Loads AGENTS.md files where Claude Code loads CLAUDE.md, in place of the built-in cc-plugin-agents-md. |
dcc-plugin-you-should-know | Opt-in. Shows one thing to know before Claude changes something, in place of the built-in cc-plugin-you-should-know. |
/plugin marketplace add xsyetopz/dotclaude
/plugin install dotclaude@dotclaude
Then run /dotclaude:setup, and restart Claude Code. The setup skill shows each change to your settings file, and it writes the change after you confirm.
Requirements:
2.1.296 or later. dotclaude is tested only on 2.1.296.git.1.4.3 or later on PATH, for the status line, the setup command, and the second_opinion tool.PATH, for the npm installs of the add-ons.A plugin cannot set permissions, environment variables, or models. So the core plugin gives these settings as a settings snippet. The setup skill applies the snippet for you. You can also copy the keys that you want into your own settings.json.
| Part | Effect |
|---|---|
| Compaction | Off. autoCompactEnabled is false, and DISABLE_COMPACT is 1, which also turns off /compact. |
| Context window | CLAUDE_CODE_MAX_CONTEXT_TOKENS and CLAUDE_CODE_AUTO_COMPACT_WINDOW are 300000. A session that reaches the limit stops, and you run /clear. |
| Memory | autoMemoryEnabled is false. The spec store holds the state instead. |
| Output style | outputStyle is dotclaude:Concise. Your own outputStyle setting can change it. |
| Permissions | Normal work passes. Work like sudo asks. Destructive actions are denied by autoMode.hard_deny. Reads of secret files are denied. |
| Sandbox | Bash runs in the sandbox. Its network reaches GitHub and the package registries only. |
| Guard | Asks before the first call of a session to a GitHub repository of another owner that has an AI policy file. |
| Asks | Asks only when a change weakens a check. It never asks for a read. |
| Status line | The folder, git, model, effort, context against the window, cost, the 5-hour, 7-day, and spend limits, and the progress of a spec change. The percent turns yellow at 75% and red at 90%. |
| Handoff | With the default manual value of mainHandover, a hint at 60% of the window tells you to run /dotclaude:handoff. |
| Checks | The CLI dotclaude-run-checks runs a batch of check commands, with no model call. |
The core plugin has one option, mainHandover. Its values are auto, manual, and off, and the default is manual.
| Agent | Model | Effort |
|---|---|---|
planner, reviewer | claude-opus-5-5 | high |
debugger | claude-sonnet-5-5 | high |
implementer, researcher | claude-sonnet-5-5 | medium |
investigator | claude-haiku-5-5 | medium |
The main session uses claude-opus-5-5, from the settings snippet. The maxEffortLevel setting is max, so /effort can go up to max for a hard check. See Agents.
The ten skills are /dotclaude:handoff, /dotclaude:rules-creator, /dotclaude:setup, and the seven spec phases /dotclaude:specify, /dotclaude:plan, /dotclaude:implement, /dotclaude:review, /dotclaude:merge, /dotclaude:accept, and /dotclaude:archive. See Skills.
Each model call reads the whole context again, so the size of the context sets the cost of each turn.
/dotclaude:specify <id>, or run dotclaude-spec specify <id>./clear, run dotclaude-spec status <id>. In the fresh session, run dotclaude-spec next <id>, which prints the brief of the next package.See Context and handoff and Spec store.
The second_opinion tool of the core plugin asks TypeSafe Jev for a pick and a confidence on a decision. Export TYPESAFE_API_KEY before Claude Code starts. Each call sends the data of the decision to the TypeSafe API. With no key, or when Jev gives no answer, a small Claude model answers instead. The result says that the answer is not from Jev. See Second opinion.
Install each add-on that you need.
/plugin install dotclaude-codegraph@dotclaude
/plugin install dotclaude-browser@dotclaude
/plugin install dotclaude-docs@dotclaude
/plugin install dotclaude-sessions@dotclaude
/plugin install dotclaude-lab@dotclaude
/plugin install dcc-plugin-agents-md@dotclaude
/plugin install dcc-plugin-you-should-know@dotclaude
dotclaude-codegraph needs the CLI codegraph on PATH, and an index that codegraph init -y builds in the project. It syncs the index at session start, and it adds no MCP server. See Code graph.dotclaude-browser uses agent-browser. Its backend option can select CloakBrowser, for sites with bot detection. See Browser.dotclaude-docs needs no key. See Library docs.dotclaude-sessions measures the start of a session and the usage of each run. See Sessions.dotclaude-lab is for plugin authors, and it works alone. See Lab.dcc-plugin-agents-md replaces the built-in cc-plugin-agents-md, so turn the built-in plugin off. See AGENTS.md.dcc-plugin-you-should-know is opt-in, because each check makes one Haiku call, on an API key or a subscription. It replaces the built-in cc-plugin-you-should-know, so turn the built-in plugin off. See You should know.claude plugin marketplace update dotclaude
claude plugin update dotclaude@dotclaude
Run the claude plugin update line again for each add-on that you installed, with its name in place of dotclaude. Then restart Claude Code, read the changelog, and run /dotclaude:setup again. The status line commands hold the path of the plugin version, so each update needs the new setup run.
To try an unreleased checkout, run:
claude --plugin-dir /path/to/dotclaude/plugins/dotclaude
Edit the source in src/, and run just generate. Do not edit plugins/ by hand. just check runs the lint, the type check, the check of the generated files, the tests, the validation, and the hook labs. just sandbox runs Claude Code with this checkout in a separate config. See Development and Sandbox.
MIT.
hooks/register.ts 12 lines1// Generated by `just generate` from `src/plugins/dotclaude-sessions/slices/*/slice.ts`. Edit the source and run `just generate`.
2import type { On } from "claude-code";
3import { onStartCost as h0 } from "../slices/start-cost/adapters/mod.ts";
4import { onUsage as h1 } from "../slices/usage/adapters/mod.ts";
5
6export const register = (on: On): void => {
7 // eval: command.run start-cost
8 on("command.run", { command: /^(?:dotclaude-sessions:)?start-cost$/ }, h0);
9 // eval: command.run usage
10 on("command.run", { command: "dotclaude-sessions:usage" }, h1);
11};
12slices/start-cost/adapters/mod.ts 36 lines1// Generated by `just generate` from `src/plugins/dotclaude-sessions/slices/start-cost/adapters/mod.ts`. Edit the source and run `just generate`.
2// The mod handler of the `start-cost` slice. It makes no model call, so `requests` is 0.
3// `command.run` answers the typed command `/start-cost` by running the CLI `dotclaude-start` and showing its output.
4// The skill stays for a call of the model, and for arguments that need a shell.
5
6import {
7 failedText,
8 outputOf,
9 wordsOf,
10} from "../../../kernel/skill-command.ts";
11import type { Handler } from "../../../kernel/slice.ts";
12
13/** The time that the CLI may take, in milliseconds. */
14const TIMEOUT_MS = 120_000;
15
16/** Runs `dotclaude-start` with the words of the command and gives the output as the answer of the command. */
17export const onStartCost: Handler<"command.run"> = async ($, e, next) => {
18 const words = wordsOf(e.args);
19 if (words === undefined) return next(e);
20 try {
21 const run = await $.process.run(
22 [
23 "bun",
24 "--no-env-file",
25 `${$.plugin.root}/bin/dotclaude-start`,
26 ...words,
27 ],
28 { timeoutMs: TIMEOUT_MS },
29 );
30 return { text: outputOf(run) };
31 } catch (error) {
32 // The catch has no narrower test, because `$.process.run` documents no error type.
33 return { text: failedText("dotclaude-start", error) };
34 }
35};
36slices/usage/adapters/mod.ts 37 lines1// Generated by `just generate` from `src/plugins/dotclaude-sessions/slices/usage/adapters/mod.ts`. Edit the source and run `just generate`.
2// The mod handler of the `usage` slice. It makes no model call, so `requests` is 0.
3// `command.run` answers the typed command `/dotclaude-sessions:usage` by running the CLI `dotclaude-usage` and showing its output.
4// The built-in `/usage` is another command, so the handler matches the name of the plugin too.
5// The skill stays for a call of the model, and for arguments that need a shell.
6
7import {
8 failedText,
9 outputOf,
10 wordsOf,
11} from "../../../kernel/skill-command.ts";
12import type { Handler } from "../../../kernel/slice.ts";
13
14/** The time that the CLI may take, in milliseconds. It reads each transcript of the period. */
15const TIMEOUT_MS = 120_000;
16
17/** Runs `dotclaude-usage` with the words of the command and gives the output as the answer of the command. */
18export const onUsage: Handler<"command.run"> = async ($, e, next) => {
19 const words = wordsOf(e.args);
20 if (words === undefined) return next(e);
21 try {
22 const run = await $.process.run(
23 [
24 "bun",
25 "--no-env-file",
26 `${$.plugin.root}/bin/dotclaude-usage`,
27 ...words,
28 ],
29 { timeoutMs: TIMEOUT_MS },
30 );
31 return { text: outputOf(run) };
32 } catch (error) {
33 // The catch has no narrower test, because `$.process.run` documents no error type.
34 return { text: failedText("dotclaude-usage", error) };
35 }
36};
37kernel/skill-command.ts 42 lines1// Generated by `just generate` from `src/kernel/skill-command.ts`. Edit the source and run `just generate`.
2// The rules of a mod handler that answers the slash command of a skill by running its CLI, with no model turn.
3// A hooks module imports this file, so it has no `node:` import, no `bun:` import, and no `Bun`.
4
5import { COMMAND_OUTPUT_MAX_CHARS } from "./budget.ts";
6
7/** A finished run, as `$.process.run` gives it. */
8export type Finished = {
9 readonly exitCode: number;
10 readonly stdout: string;
11 readonly stderr: string;
12};
13
14/** A character that a shell reads, and that `$.process.run` does not: quotes, escapes, and expansions. */
15const SHELL_CHAR = /["'`\\$;&|<>(){}*?]/;
16
17/**
18 * The words of the argument text of a command, or undefined when the text needs a shell to read it.
19 * `$.process.run` takes an argument vector with no shell, so a quote would reach the CLI as a character.
20 * The caller leaves such a command to the skill.
21 */
22export const wordsOf = (args: string): string[] | undefined =>
23 SHELL_CHAR.test(args) ? undefined : args.split(/\s+/).filter(Boolean);
24
25/** `text`, cut to `COMMAND_OUTPUT_MAX_CHARS` with a last line that says how much was cut. */
26const bounded = (text: string): string =>
27 text.length <= COMMAND_OUTPUT_MAX_CHARS
28 ? text
29 : `${text.slice(0, COMMAND_OUTPUT_MAX_CHARS)}\n[${text.length - COMMAND_OUTPUT_MAX_CHARS} characters cut]`;
30
31/** The text that a command shows for the run `run` of its CLI: the output, or the status and the error text. */
32export const outputOf = (run: Finished): string => {
33 if (run.exitCode === 0)
34 return bounded(run.stdout.trim() || "The command printed nothing.");
35 const error = (run.stderr || run.stdout).trim();
36 return `The command failed with exit status ${run.exitCode}.\n${bounded(error)}`;
37};
38
39/** The text that a command shows when its CLI did not finish, such as on a timeout. */
40export const failedText = (name: string, error: unknown): string =>
41 `${name} did not finish: ${error instanceof Error ? error.message : String(error)}`;
42kernel/slice.ts 114 lines1// Generated by `just generate` from `src/kernel/slice.ts`. Edit the source and run `just generate`.
2// The contract of a slice manifest: what a slice folder exports from its `slice.ts`.
3// The generator reads each manifest, and the types of the mod API come from `claude-code`.
4// This file has no runtime import, so the hooks module can load it.
5
6import type {
7 Args,
8 EventName,
9 Events,
10 Matcher,
11 PluginOptions,
12} from "claude-code";
13
14/** The order of the phases of a mod handler, from the first to the last. The generator sorts by it. */
15export const PHASES = [
16 "observe",
17 "normalize",
18 "rewrite",
19 "check",
20 "answer",
21] as const;
22
23/** One phase of a mod handler, from `PHASES`. */
24export type Phase = (typeof PHASES)[number];
25
26/** The filter of a mod handler: a matcher over the argument of the event `E`. */
27export type EventFilter<E extends EventName = EventName> = Matcher<Args<E>>;
28
29/** The hook signature of the event `E`, as the mod API declares it. */
30export type Handler<E extends EventName = EventName> = Events[E];
31
32/**
33 * Keeps the `userConfig` values of the plugin, which the mod runtime gives to `register`, for a hook to read.
34 * The generated `register` calls it once, before the `on()` calls.
35 * The hook itself goes to `on()` by its name, because `claude plugin validate --strict` follows `$` only into a function of `register.ts`.
36 */
37export type Configure = (options: PluginOptions) => void;
38
39/**
40 * The eval kinds of one hook, which the generator writes to `evals/hooks.json` under the id `<event> <slice>`.
41 * A test is `<file from the repository root> > <test name>`.
42 * A hook that runs a model has `requests` above 0 and names its `claude plugin eval` case in `eval`.
43 * A hook that can block also names `block` and `false`.
44 */
45export interface HookEvals {
46 readonly requests: number;
47 readonly tokens: number;
48 readonly blocks: boolean;
49 readonly allow: string;
50 readonly text: string;
51 readonly block?: string;
52 readonly false?: string;
53 readonly eval?: string;
54}
55
56/**
57 * One mod handler of a slice. A slice can have more than one for one event, each with its own filter.
58 * It has `run`, the hook, and `configure`, when the hook reads the `userConfig` values of the plugin.
59 */
60export interface ModHandler<E extends EventName = EventName> {
61 readonly event: E;
62 readonly filter?: EventFilter<E>;
63 readonly phase: Phase;
64 /**
65 * The hook. A hook with a `filter` can take the event that the filter narrows, as `MatchedHook` of the mod API types it.
66 * The generated `register.ts` gives the hook and the filter to `on()`, and the compiler checks the pair there.
67 */
68 readonly run: Handler<E> | ((...args: never[]) => unknown);
69 readonly configure?: Configure;
70 /** The eval kinds of the hook. One entry covers each handler of the slice on the same event. */
71 readonly evals?: HookEvals;
72}
73
74/** One classic command hook, for an event that the mod API does not cover. */
75export interface ClassicHook {
76 readonly event: string;
77 readonly matcher?: string;
78 readonly command: string;
79 readonly timeout?: number;
80 readonly async?: boolean;
81 /** The eval kinds of the hook. Its id is the event and the command, without the `bun` launcher and the plugin root. */
82 readonly evals?: HookEvals;
83}
84
85/** One segment of the status line. The `row` is the row of the status line that holds it. */
86export interface StatusSegment {
87 readonly id: string;
88 readonly row: number;
89 readonly render: (status: unknown) => string | null;
90}
91
92/** One `userConfig` field of a plugin manifest, such as the `backend` of `dotclaude-browser`. */
93export interface UserConfigOption {
94 readonly type: "string" | "number" | "boolean";
95 readonly title: string;
96 readonly description?: string;
97 readonly default: string | number | boolean;
98 readonly options?: readonly string[];
99}
100
101/** The manifest of one slice: the name, and each part that the slice adds to the plugin. */
102export interface Slice {
103 readonly name: string;
104 readonly mod?: readonly ModHandler[];
105 readonly hooks?: readonly ClassicHook[];
106 readonly cli?: { readonly bin: string };
107 readonly segments?: readonly StatusSegment[];
108 readonly skills?: readonly string[];
109 readonly agents?: readonly string[];
110 readonly outputStyles?: readonly string[];
111 readonly userConfig?: Readonly<Record<string, UserConfigOption>>;
112 readonly settings?: Readonly<Record<string, unknown>>;
113}
114kernel/budget.ts 71 lines1// Generated by `just generate` from `src/kernel/budget.ts`. Edit the source and run `just generate`.
2// The usage bounds of dotclaude, in one place.
3// Tests pin the copies in the agent files to these values,
4// so change a bound here and nowhere else.
5
6/**
7 * The context window in tokens.
8 * The settings snippet sets `CLAUDE_CODE_MAX_CONTEXT_TOKENS` to it,
9 * which Claude Code honors only together with `DISABLE_COMPACT=1`.
10 * At this size the session stops with `blocking_limit`, and does not compact.
11 * The snippet also sets `CLAUDE_CODE_AUTO_COMPACT_WINDOW` to it.
12 * Without it, Claude Code 2.1.296 shows and warns against a 200K window for Opus 5.5,
13 * because that model is on its "billed past 200K" list.
14 */
15export const CONTEXT_WINDOW = 300_000;
16
17/**
18 * The window in tokens that a status object reports, at most `CONTEXT_WINDOW`.
19 * A `size` that is absent, not a finite number, or not above zero gives `CONTEXT_WINDOW`.
20 */
21export const windowOf = (size: number | null | undefined): number =>
22 typeof size === "number" && Number.isFinite(size) && size > 0
23 ? Math.min(size, CONTEXT_WINDOW)
24 : CONTEXT_WINDOW;
25
26/**
27 * The tokens below `CONTEXT_WINDOW` at which the client blocks the next request with `Prompt is too long`:
28 * the output reserve of 20K tokens plus 3K tokens (2.1.296 bundle).
29 */
30export const CONTEXT_BLOCK_MARGIN = 23_000;
31
32/** The status line shows the context in yellow from the first and in red from the second percentage of `CONTEXT_WINDOW`. */
33export const USAGE_LEVELS: [number, number] = [75, 90];
34
35/**
36 * The characters of a successful `Bash` output that Claude receives inline, the settings key `bashOutputMaxChars`.
37 * Claude Code saves a longer output to a file and sends a preview and the path, so no output is lost.
38 * 4000 is the lowest value that Claude Code accepts (the 2.1.296 schema clamps to 4000 to 128000).
39 */
40export const BASH_OUTPUT_MAX_CHARS = 4_000;
41
42/** The characters of the CLI output that a skill command shows with no model turn. A longer output is cut, and a line says how much. */
43export const COMMAND_OUTPUT_MAX_CHARS = 20_000;
44
45/** The time for one `gh` call of the policy guard. */
46export const POLICY_TIMEOUT_MS = 5_000;
47
48/** The characters of one policy file in the ask prompt. */
49export const POLICY_FILE_MAX_CHARS = 2_000;
50
51/**
52 * The most characters that `commands` in `src/kernel/shell.ts` reads in one command, with all nested scripts.
53 * A longer command is read only in part, and `parse` reports it as `truncated`.
54 */
55export const SHELL_SCAN_MAX_CHARS = 200_000;
56
57/**
58 * The most words of one command that a code reader reads from code in another language.
59 * A git command or a write form needs few words, and the bound keeps the cost linear in the length of the code.
60 */
61export const CODE_COMMAND_MAX_WORDS = 32;
62
63/**
64 * The tries of a rename in `writeAtomic`, the first one included.
65 * On Windows, a rename over a file that another process holds open, such as an antivirus, an indexer, or an editor, can fail for a short time.
66 */
67export const RENAME_TRIES = 10;
68
69/** The wait in milliseconds between two tries of a rename in `writeAtomic`, so the total wait is short. */
70export const RENAME_WAIT_MS = 100;
71