Skip to content
Chock
Configuration (17 of 30)

Configuration

Chock reads ~/.config/chock/config.zon and never writes it, so you can give the whole directory to home-manager.

.{
    .providers = .{
        .{ .name = "local", .kind = "openai-compat", .base_url = "http://127.0.0.1:5000/v1" },
        .{ .name = "chatgpt", .kind = "openai" },
        .{ .name = "work", .kind = "aiand" },
    },
    .defaults = .{ .provider = "local", .model = "glm4.7-flash:A3B" },
}

The kinds are anthropic, aiand, openai, and openai-compat. The openai kind uses OpenAI’s Responses API and a ChatGPT web login. An openai-compat instance uses Chat Completions and also needs a base_url.

The name is the identity

The name is the key and the kind is a property, so two accounts of one kind sit side by side under different names. Leave .name out and the kind becomes the name. Two instances of one kind with no name are refused, and the message says a name is needed, because silently replacing the first is how somebody loses a credential they cannot get back.

How much the model holds

Add .context_tokens to say how much the model behind an instance can hold:

.{ .name = "local", .kind = "openai-compat", .base_url = "http://127.0.0.1:5000/v1", .context_tokens = 65536 }

Chock then folds the middle of the context into a summary at three quarters of that, before a turn is sent. It tells the agent at three fifths, which is a turn or more earlier, so the agent can save what it learned first. Leave it out and only the backstop runs: when a provider refuses a request as too large, the context is compacted and the turn is taken again, rather than the session ending. Either way the session log keeps every turn, so a compaction shortens what the model reads and nothing else.

What one instance can do

.capabilities says what an instance can do beyond answering with words. Every field of it defaults to false, and two instances of one kind may differ:

.{ .name = "personal", .kind = "aiand", .capabilities = .{ .images = true } }

A tool is offered to the model only when the adapter can express it and the provider instance does it.

Run chock models to see the effective local view: provider instances, the default model, context limits, and the features each adapter can faithfully carry. chock models --provider <name> narrows that report to one instance. The command does not read credentials or contact the provider, so it reports configured and adapter capabilities rather than making an unverified claim about a remote model. Provider-side search is named as unavailable; web search continues to use Chock’s separately configured, policy-controlled tool.

Add --probe to opt into credential lookup and one authenticated GET /models request per selected provider. The returned model identifiers are sorted before they are printed. chock models --provider <name> --probe is the narrow form. The same typed catalogue result is kept in chock-auth, so the daemon and graphical clients can use it for a model switcher without parsing terminal output or handling credentials themselves.

Naming a credential

A provider names its credential one of three ways:

.{ .name = "work", .kind = "aiand", .token_file = "/run/secrets/aiand" }  // sops-nix, agenix
.{ .name = "work", .kind = "aiand", .token = "sk-..." }                   // in place, mode 0600 only
.{ .name = "work", .kind = "aiand" }                                      // look it up, and send none if there is none

An instance that gives both .token and .token_file is refused, because which one to read would not be decided. A field that is present and holds nothing is refused too: absence already means “look it up”, so “the field is not there” and “the field is there and holds nothing” stay two different facts.

The third line above is what a local endpoint needs. chock run still refuses it for an instance of kind anthropic, aiand, or openai that talks to that kind’s own address, because those addresses refuse every request that carries no credential. The refusal comes before anything is built, so no workspace and no log are left behind.

Use the third spelling for openai, then run chock login --provider openai. An OpenAI API key is not a credential for this kind. Use openai-compat with .token_file when you want to call an OpenAI-compatible endpoint with an API key.

Where a lookup goes, and what chock login writes, is in credentials.md.

The machine’s own resource limits

config.zon also takes a limits block, in the same shape a project’s own chock.zon takes one:

.{
    .limits = .{
        .processes = "50%",
        .memory = "4GiB",
    },
}

This is the machine’s own default: every project run from this machine that names no limits field of its own gets the number here instead of Chock’s own machine sized default. A project’s own chock.zon still wins over it, the same way a project’s own choices win over everything else this file sets. See Sandbox resource limits for the field syntax and the full order the layers fold in.

The machine’s own Nix store caps

config.zon also takes a nix block, in the same shape a project’s own chock.zon takes one:

.{
    .nix = .{
        .max_object_bytes = "16MiB",
        .max_session_bytes = "256MiB",
    },
}

This is the machine’s own default for how much a Nix evaluation may add to the store: every project run from this machine that names no nix field of its own gets the number here instead of Chock’s own built in default. A project’s own chock.zon still wins over it. See Nix store byte caps for the field syntax and the full order the layers fold in.

The project’s own file

config.zon is yours and it is about providers. chock.zon, in the project root, is the project’s own file and it is about what an agent may do there. What an agent may do lives in it: the policy table, the budget, the subagent limits and the denied paths, and so do the plugin list, the MCP servers, the language servers and the container image. Chock binds it into the workspace read only, so the agent works under rules it cannot edit.

The whole file is one struct literal, and each block is one field of it. This is a complete chock.zon with every block a first project needs:

.{
    .policy = .{
        .agents = .{
            .{ .kind = "main" },
        },
        .rules = .{
            .{ .action = "net.fetch.org.ziglang", .decision = .allow },
            .{ .action = "git.push", .decision = .deny },
        },
    },
    .budget = .{ .max_cost = 5.0, .currency = "USD" },
    .deny_read = .{
        ".env",
    },
    .subagents = .{ .max_width = 2 },
    .context = .{ .compact_at_percent = 80 },
    .apply = .{ .mode = .merge },
}

.subagents also takes .max_depth. Both default to 6. See subagents.md.

The context block controls when the transcript is compacted and how much of its recent tail survives unchanged:

.{
    .context = .{
        .warn_at_percent = 60,
        .compact_at_percent = 75,
        .keep_recent_entries = 6,
        .keep_recent_kib = 16,
        .summary_max_kib = 16,
    },
}

The percentages apply to the provider’s model limit. The warning must precede compaction; compaction is bounded to 10–95%, at most 64 recent entries and 256 KiB of recent text, and a summary of at most 64 KiB. Leaving the block out uses the values above. Files supplied with --instructions, AGENTS.md, and files in the instructions block are pinned in the system prompt rather than the compactable transcript. Each file is already bounded to 8 KiB, and its operator/session/project layer fixes precedence; pinning one cannot lift an organisation policy or turn project text into operator instructions.

Inspect the effective result with chock sessions show <session> --only context. The log records the threshold and every retained, summarized, or dropped event range, so replay gives the same provenance.

.apply.mode says how an approved apply lands. It defaults to merge, which parks the work at refs/chock/<session> and then merges it into the branch you have checked out. The approval prompt names the branch and the landing before you answer, so the y you give is a y to that act. There is no mode that means “move nothing”: say n to the apply, or write the policy row below. approvals.md shows both prompts.

A rule goes under .policy.rules, and never directly under .policy. .policy is a struct with named fields, so a rule written beside .agents is a syntax error and the session does not start. Every block is optional: leave out the ones you do not want.

Every block of this file narrows what an agent may do, and two rows of the policy table widen: sandbox.jit and workspace.integrate. Each is a row and not a block of its own, because the table is where an organisation can forbid what a project cannot take back. actions.md has both.

  • policy.md for how a rule is read, and for the denied paths.
  • actions.md for every action name a rule can carry.
  • secrets.md for the secrets block, which says what a tool call may be given without the agent ever seeing it.
  • skills.md for the skills block, which says which directories of this project hold skills.
  • commands.md for the commands block, which says which directories of this project hold the messages a person can run by name.
  • hooks.md for the hooks block, which says what this project runs at a point in a session.
  • remote-mcp.md for an mcp_servers entry that names a url rather than a command, and the rules above it.
  • org.md for the bundle an organisation puts above the project.
  • subagents.md for the subagent limits.
  • plugins.md for the plugin list.

The project’s own skills

The skills block names directories that hold skill directories. Each path must be under the project and must not climb out of it:

.{
    .skills = .{ ".chock/skills" },
}

There can be at most four. Leave the block out and the project ships none, which is what every project does today. skills.md has the format and the three layers.

The project’s own commands

The commands block names directories that hold a project’s commands, and it is read by the same reader with the same path checks:

.{
    .commands = .{ ".chock/commands" },
}

A command is a message a person runs with chock run /<name>, so this block adds nothing to what the agent may do. commands.md has the format and the arguments.

The project’s own hooks

The hooks block names programs this project runs at a point in a session:

.{
    .hooks = .{
        .{
            .name = "tests-pass",
            .on = "tool.before",
            .tool = "request_action",
            .run = .{ "zig", "build", "test" },
            .output = "refuse",
        },
    },
}

A hook runs through the gate a tool call goes through, so it narrows what a session may do and never widens it. There can be at most eight. hooks.md has the triggers, the fields and what the log holds.

The project’s own style rules

The style block names which grammars this project has and which of its own tree-sitter queries refuse a write:

.{
    .style = .{
        .parsers = "TREE_SITTER_PARSERS",
        .grammars = .{
            .{ .language = "zig", .extensions = .{".zig"} },
        },
        .rules = .{
            .{ .name = "comment-block", .query = "style/comments.scm", .max_lines = 10 },
        },
    },
}

parsers names an environment variable and never a path, because a store path changes whenever the grammar package is bumped. A rule carries exactly one bound, and style.parse in the policy table decides whether any rule runs at all. There can be at most 32 rules and 16 grammars.

Two more keys pick the parser. engine is command, which runs tree-sitter, or wasm, which runs a module inside Chock’s own engine and needs no tree-sitter on the host. modules names the variable holding the parser modules, one <language>.wasm a language, and defaults to CHOCK_PARSER_MODULES. style.md has the bounds, the two decisions, what the log holds, and what the wasm engine costs before the first write.

Which way of sandboxing a session uses

config.zon is the only file that names this, and that is deliberate: a project may only narrow what it may do, and a project that could name its own sandbox driver could name a weaker one.

.{
    .sandbox = .{ .driver = "microvm" },
}

native is the default and is today’s behaviour. See microvm.md for what a guest adds, and org.md for how an organisation pins or forbids one.

How big a guest is

The same block names how many processors a guest gets and how much memory. Both are optional, and a number here is used as it is.

.{
    .sandbox = .{ .driver = "microvm", .cores = 4, .memory_mb = 4096 },
}

Leave them out and both scale with the machine. A host with 128 cores and 511GB gives a guest 12 processors and 12GB, one with 8 cores and 8GB gives 2 processors and 1GB, and a guest never gets more than one eighth of the memory of the machine it runs on. The processor count is also limited by that share of memory, because a build runner starts one job for each processor and a guest with more processors than memory loses a compiler to the kernel. cores is further limited to 8 on a Mac, where the interrupt controller a guest gets has no more than that. The curves and the reasons for them are in microvm.md.

The dev shell the agent gets

The nix block of chock.zon names which devShells attribute the tool environment comes from:

.{
    .nix = .{
        .dev_shell = "ci",
    },
}

Leave it out and Chock reads default, which is what nix develop reads. --dev-shell <name> overrides it for one run. A name no flake carries stops the session with Nix’s own message. See toolchains.md.

Where a provisioned tool may come from

The same block says what a tool provide_tool builds must say about its own origin:

.{
    .nix = .{
        .provenance = .{
            .required = true,
            .from = .{"https://cache.flakehub.com"},
            .built_by = .{"builder-one"},
        },
    },
}

Left out, nothing is asked and nothing is read. What each field means, and what a refusal says, is in tools.md.

Paths the worktree does not carry

The agent works in a git worktree at HEAD, so a file git ignores is invisible to it. A generated configuration and an automation directory are the two such paths a real project tends to need. The workspace block names them:

.{
    .workspace = .{
        .binds = .{
            .{ .name = "config.local.*", .mode = .read_only },
            .{ .name = "scripts/release", .mode = .copy, .write = .ask },
            .{ .name = "vendor/cache", .mode = .temp_copy },
            .{ .name = "generated/maybe", .mode = .read_only, .required = false },
        },
    },
}

Each bind names a path under the project root. The mode says how it reaches the agent and what happens to a change the agent makes to it:

Mode How it arrives What happens to a change
read_only a bind mount, read only impossible
write a bind mount, read write lands on your real file as it is written
copy copied into the workspace written back when the work applies
temp_copy copied into the workspace discarded with the workspace

mode is required and has no default. Every value of it decides how much of your own disk the agent reaches, and a default would make that invisible.

A bind can name a directory. read_only and write bind the directory and the two copying modes copy it whole.

What may be written back

write carries the same three answers a policy rule carries: allow, ask and deny. It is meaningful for write and copy alone, because on read_only or temp_copy it is refused when the file is read: neither of those reaches your files at all. Leave it out and it is ask.

The policy table decides beside it, under the action workspace.bind.<name>, and the narrower of the two answers wins. So an organisation can refuse the whole mechanism with one row, and you can deny one path without editing a file somebody else wrote. See actions.md.

The moment the question is asked differs by mode, because it is asked where it can be:

  • A write bind is decided at session start, before the bind is made. It is read only until something answers allow, and the session start says so.
  • A copy bind is decided at the end, in the workspace.apply prompt, which names every file that is copied back. Refuse that apply and your files stay as they are, with the session’s copies left in the workspace.

Names, patterns and what is refused

A name holding * or ? is a pattern. * matches inside one path component, ** matches any run of components, and ? matches one character that is not a separator. Every other character matches itself. The patterns run against the project directory, and never against git’s list of ignored files, so a name that happens to be tracked already is simply in the workspace twice over.

required says whether a name matching nothing stops the session. Its default is derived: true for a name with no pattern character, false for a pattern. A person who wrote scripts/release asked for that path, while a pattern matching nothing is ordinary. Write .required yourself to say otherwise, in either direction.

A match may be a symbolic link. Chock resolves it and binds the real file, because a mount source cannot be a link. Every resolved path has to stay inside the project: a name holding .., an absolute name, and a link that reaches out of the project are each refused by name, and the session does not start. So are chock.zon and anything under .git, which Chock holds for itself.

The session start names every bind it made, with what the name resolved to and the mode it arrived under.

When Chock refuses the file

A fault in chock.zon stops the session before the agent runs. The message names the file, the line and the column.

Chock reads the file as a whole, and reads the deny_read block, before it builds the workspace. A fault that early is named against the workspace:

chock run: the workspace for /home/you/site could not be built: chock.zon is
not valid:
4:9: error: expected field initializer

Every other block names its own block instead. The policy and workspace blocks are read before the workspace is built, because a bind is decided against the policy table. The budget, subagents and plugins blocks are read after it is there:

chock run: the budget in chock.zon could not be read: chock.zon: the budget
block is not valid:
1:18: error: unexpected field

A message that says chock.zon is not valid is about the whole file, and the fault can be in any block of it. A message that names a block, such as the deny_read block is not valid or the budget block is not valid, is about that block alone.

A project with no chock.zon has no spend cap. The two defaults run in opposite directions on purpose: an action no rule names resolves to ask, so a project that wrote no policy permits nothing by itself, while a budget nobody wrote is no budget, so a session runs until it finishes. Write a budget block to bound what a session may spend:

.{ .budget = .{ .max_cost = 5.0, .currency = "USD" } }

The cap lives in that file and nowhere else, so the model cannot raise it. A misspelled field inside the block is refused rather than quietly read as no cap.