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 = "work", .kind = "aiand" },
},
.defaults = .{ .provider = "local", .model = "glm4.7-flash:A3B" },
}
The kinds are anthropic, aiand, and openai-compat. An openai-compat
instance 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 with 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: a provider that refuses a request as too large is compacted and the turn is taken again, rather than ending the session. 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.
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 is not 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 or aiand that talks to that kind’s
own address, because those two refuse every request that carries no
credential. It refuses before it builds anything, so no workspace and no log
are left behind.
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 which
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 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 which 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. 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 },
.apply = .{ .mode = .merge },
}
.subagents also takes .max_depth. Both default to 6. See
subagents.md.
.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
secretsblock, which says what a tool call may be given without the agent ever seeing it. - skills.md for the
skillsblock, which says which directories of this project hold skills. - 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" },
}
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 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.
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 a real
project needs anyway. 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. On read_only or
temp_copy it is refused when the file is read, because 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
writebind is decided at session start, before the bind is made. It is read only until something answersallow, and the session start says so. - A
copybind is decided at the end, in theworkspace.applyprompt, 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, and 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. 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 read as no cap.