Skip to content
Chock
Secrets for a tool (15 of 23)

This page is the documentation for unstable. Read the current version.

Secrets for a tool

An agent can be useful with the GitHub CLI without ever holding a GitHub token. The secrets block says which secret one tool call may be given, and Chock gives the value to the program and not to the agent.

The mapping is yours to write

The block goes in the project’s own chock.zon, beside the policy:

.{
    .secrets = .{
        .{ .name = "GITHUB_TOKEN", .to = "exec.path.gh" },
    },
}

Nothing is discovered. An agent has no way to ask for a secret this block does not name, and no way to add one: it reads the file it cannot write.

There is no operator layer and no org layer for this block. An org bundle is a ceiling and can only narrow what a project may do, so nobody else can add an entry that gives a project’s tools something new.

What an entry says

Field What it means
name The secret, under the name the credential store knows it by.
to The action name this secret may be given to.
bind How it arrives: env, or file. env is the default.
as The variable it arrives under. Left out, the secret’s own name.

to is an action name pattern, read by the same table every other permission uses. See actions.md. So exec.path.gh reaches one program, and exec.* reaches every program the agent may run. An entry names an action and never a command string, because a command string says nothing about what the command will do.

Letters, digits and underscore make a name. A name becomes part of the action secret.use.<name>, so a dot would let one entry name a class of actions nobody wrote.

Using one is asked about

Every use is a brokered action, under secret.use.<name>. The block says the secret may reach gh; the policy says whether that is a standing permission or a question every time:

.{
    .policy = .{
        .allow = .{"secret.use.GITHUB_TOKEN"},
        .ask = .{"secret.use.DEPLOY_KEY"},
    },
}

One action per secret, so the one that matters can be a question while the rest are not. policy.md has the rest of the table.

Putting the value where Chock reads it

The secret goes in the credential store, under the name the block gives:

chock login --tool-secret GITHUB_TOKEN

There is no option that takes the value itself. A command line is visible to every other user through ps, and it lands in the shell history.

The name has no prefix of Chock’s own, so the store holds it under exactly the name a SecretSpec profile would. If your credentials block names secretspec, name the secret in your profile and there is nothing to log in for. See credentials.md for the stores.

Arriving as a file

Some programs will not read a credential out of the environment at all, and want a path to a file. bind = "file" is that:

.{
    .secrets = .{
        .{
            .name = "GCP_KEY",
            .to = "exec.path.gcloud",
            .bind = "file",
            .as = "GOOGLE_APPLICATION_CREDENTIALS",
        },
    },
}

The variable then names the file and not the value. Chock writes the file before the call starts, mounts it read only inside the sandbox, and deletes it when the call ends.

The file is written under TMPDIR on the host, mode 0600, for as long as the one call runs. That is a real difference from env: the value is on a filesystem for that time, and a delete is a delete and not an erase. A program that reads an environment variable should be given one.

An MCP server is not given a file. It holds what it is given for a whole session, so the file would have to live that long too, and Chock says so and gives the server nothing rather than writing one.

What the agent sees

The value reaches the environment of one tool call and nothing else. It is not in the prompt, and the environment of the next call does not have it.

The environment is not what hides it. An agent picks the argument list, so it can ask a program to print its own token. What hides the value is redaction: every tool result is read for it before the result reaches the log or the model, and it is replaced. Chock keeps a redaction slot for every secret a call may hold, and a call granted more secrets than there are slots is refused rather than run unprotected.

An MCP server holds one for its whole life

A server is started once and reads its environment once, so it is given its secrets at start rather than per call:

.{
    .secrets = .{
        .{ .name = "GITHUB_TOKEN", .to = "mcp.github.*" },
    },
}

mcp.github.* names one server, and mcp.* names every server. An entry that reaches nothing under a server’s own namespace gives that server nothing, so one server’s secret does not reach another.

A server starts before the loop does, so there is nobody to ask. Only allow gives a server a secret: an ask on secret.use.<name> reaches nobody at that moment, so it means the server is not given it, and Chock says which rule read that way. nix.build and model.select are read the same way, for the same reason. actions.md has both.

The slot that keeps a server’s secret out of the log is its own, and it stays filled for the whole session. A tool call’s slot is cleared when that call ends.

What is written down

Each use appends a secret.used event to the session log: the secret’s name, the action it was given to, how it bound, and the variable it arrived under. The value is not in the event, and there is no field it could travel in.

{"kind":"secret.used","name":"GITHUB_TOKEN","action":"exec.path.gh","bind":"env","variable":"GITHUB_TOKEN"}

What this does not do yet

  • A plugin is given nothing. An entry naming plugin.* reaches nothing today.
  • A background command is given nothing. It outlives the call it was started from, and a grant that outlived its call would reach work nobody approved it for.