Files and Branches
Give an agent its own copy of your files to rewrite, compare the versions, and accept only the ones you want — the runtime never writes your real files.
Most of the time you do not want an agent editing your files. You want it to try an edit, show you the result, and leave the original alone until you agree.
That is what a branch is. You pick a set of files, the runtime reads a copy of them into its own store, and every edit happens to that copy. Your files on disk are never written during this. You compare the proposed version against the current one, and only then advance the branch.
What a branch actually holds
A branch is a named list of {path → content ref} entries — a manifest.
A content ref is a 64-character hash of the file's exact bytes. The hash is the identity: two files with identical contents get the same ref, and any change at all produces a different one. So comparing two versions of a file starts with comparing two short strings.
A branch is named with a three-part handle — namespace/collection/name, for
example team/workspace/main. Branches are scoped to whoever created them. If
you ask for a branch you do not own, the answer is the same "not found" you would
get for a branch that does not exist.
Copy-on-write, by construction
The store is content-addressed, so forking a branch copies nothing. A fork inherits its parent's refs and only re-points the paths that later change. Unchanged files keep pointing at the same bytes.
Reading your files is off by default
The runtime cannot read anything on your filesystem unless you turn that on.
You turn it on by setting KX_SERVE_FS_ROOT to one directory when you start the
runtime. That directory becomes the only place file reads may reach — paths that
try to climb out of it with .. or a symlink are refused. Each path must also be
a regular file under a per-file size cap; an oversized path is refused rather
than truncated. With the variable unset, taking a snapshot fails with a
FAILED_PRECONDITION error and nothing on your disk is touched.
KX_SERVE_FS_ROOT=/path/to/workspace kx serve --dev-allow-localTwo things worth being precise about, because they are the whole safety story:
- Read-only. Snapshotting reads bytes in. The branch flow never writes back out. An agent editing a branch file cannot reach your disk at all, whatever it proposes.
- Writing back is a separate switch. There is a distinct host-write tool,
gated behind its own separate environment variable (
KX_SERVE_FS_WRITE_ROOT), confined the same way, and staged so an approval gate can hold it for a human. Turning reads on never turns writes on. See /docs/approvals.
Snapshot files into a branch
Start the runtime with a read root
KX_SERVE_FS_ROOT=/path/to/workspace kx serve --dev-allow-local--dev-allow-local is the loopback-only development mode. A bare kx serve
refuses to start unauthenticated. For anything else, pass --auth-token — see
/docs/serving.
Snapshot a set of paths
Each --path is relative to the read root. The branch is created on the first
snapshot, so you do not have to create it separately.
kx branch snapshot team/workspace/main --path src/lib.rs --path README.mdfrom kortecx import KxClient
with KxClient("http://127.0.0.1:50151", token="...") as kx:
snap = kx.snapshot_into("team/workspace/main", ["src/lib.rs", "README.md"])
print(snap.ingested, len(snap.items)) # files read, manifest sizeimport { KxClient } from "@kortecx/sdk";
const kx = new KxClient("http://127.0.0.1:50151", { token: "..." });
const snap = await kx.snapshotInto("team/workspace/main", ["src/lib.rs", "README.md"]);
console.log(snap.ingested, snap.items.length);Look at the manifest
kx branch get team/workspace/mainYou get the branch's own ref, its file count, and one row per path with that
file's content ref. Add --json if you want to keep the refs for a later
comparison.
# abridged — the real refs are 64 hex characters and the header ends with the branch description
team/workspace/main ref=9f2c… 2 file(s)
README.md -> 4b1e…
src/lib.rs -> c07a…To see every branch you have, in handle order:
kx branch listFork before you experiment
A sub-branch is a point-in-time fork. It inherits the parent's refs as they are right now, and later changes to the parent do not reach it. This is the cheap way to keep a known-good version pinned while an agent works on a copy.
kx branch create team/workspace/feature --parent team/workspace/main
kx branch snapshot team/workspace/feature --path src/lib.rsOnly src/lib.rs re-points; every other path still points at the parent's bytes.
When you are done with the experiment:
kx branch remove team/workspace/featureRemoving a branch unbinds the name. The stored bytes stay, so anything else pointing at them is unaffected.
Let an agent rewrite a file
kx branch edit attaches the file's current contents, asks a model to apply your
instruction, and points the branch at the result. The rewrite happens entirely in
the store.
kx branch edit team/workspace/main --path README.md \
--instruction "Add a one-line summary at the top; keep the rest unchanged"res = kx.edit_branch("team/workspace/main", "README.md",
"Add a one-line summary at the top; keep the rest unchanged")
print(res.handle, res.branch_ref) # the manifest advancedconst res = await kx.editBranch(
"team/workspace/main",
"README.md",
"Add a one-line summary at the top; keep the rest unchanged",
);This needs a model served by the runtime — see
/docs/serving and /docs/local-inference.
The rewrite is a single model step with a generous default timeout of 300
seconds, adjustable with --timeout-secs.
A few behaviours that matter in practice:
- The committed file is the model's answer, verbatim. Leading reasoning is stripped at commit; nothing else is transformed.
- It fails closed. If the model returns no usable file body, the command errors and the branch is left exactly as it was. It never advances to an empty file.
- Only the target file is attached. The model rewrites from the contents you gave it. It is not browsing your directory.
- Quality depends on the model. A model that stops short of finishing the rewrite gives you an error to re-run, not a corrupted file. Nothing retries on your behalf.
Then check what moved:
kx branch get team/workspace/mainREADME.md now points at a different ref. src/lib.rs does not.
Compare versions before you accept
The one-shot edit above rewrites and advances together. When you want to read
the proposal first, the SDKs split it in two: propose, look, then advance.
The propose step returns both the current text and the proposed text, plus the ref the proposal was committed under. Nothing has moved yet — if you do not advance, the proposal is simply an unreferenced blob in the store.
# The CLI's `edit` advances in one shot. To advance by hand, point a path
# at a ref you already have:
kx branch advance team/workspace/main --path README.md --ref <64-hex content ref>prop = kx.edit_branch_propose("team/workspace/main", "README.md",
"Add a one-line summary at the top")
print(prop.current_text) # what the branch holds now
print(prop.proposed_text) # what the model wrote
if input("accept? ") == "y":
kx.advance_branch("team/workspace/main", "README.md", prop.result_ref)const prop = await kx.editBranchPropose(
"team/workspace/main",
"README.md",
"Add a one-line summary at the top",
);
console.log(prop.currentText); // what the branch holds now
console.log(prop.proposedText); // what the model wrote
// accept it:
await kx.advanceBranch("team/workspace/main", "README.md", prop.resultRef);You can also read any branch file's current body directly —
get_branch_content(handle, path) in Python, getBranchContent(handle, path) in
TypeScript — and diff it against whatever you like with your own tools.
advance is the low-level verb
kx branch advance re-points a path at a ref that already exists in the store.
It does no rewriting and asks no questions. Use edit for the normal flow; reach
for advance when you are scripting or accepting a proposal you already
reviewed.
The subcommands
| Command | What it does |
|---|---|
kx branch create <handle> [--parent <handle>] [--description <text>] | Create a branch, or fork one from a parent. |
kx branch snapshot <handle> --path <p> [--path <p>…] [--parent <handle>] [--description <text>] | Read confined host files into the branch. Creates it if absent (--parent/--description apply only then). |
kx branch list | List your branches, in handle order. |
kx branch get <handle> | Show the resolved {path → ref} manifest. |
kx branch edit <handle> --path <p> --instruction <text> [--timeout-secs <n>] | Rewrite one file with a model, in the store, and advance the manifest. |
kx branch advance <handle> --path <p> --ref <64-hex> | Re-point a path at an existing content ref. |
kx branch remove <handle> | Unbind the branch. Its stored bytes stay. |
--json works on all of them for machine-readable output.
Where branches sit in the system
The branch index lives in a sidecar database, deliberately outside the run journal. It is never part of a run's identity or digest. If you lose that file you lose the index and re-snapshot to rebuild it — you cannot lose or corrupt the record of what your runs actually did. Branch identity itself is derived by the server from the handle you give it, not claimed by the client.
Authoring a Connector
Scaffold an MCP connector crate with one command, build it offline in fake mode, and check it against the same conformance gate the repository's CI runs.
Secrets
Store a password or API token once in your computer's own keychain, then refer to it by name everywhere — the value is write-only and no request ever returns it.