Profile file reference
Look up every Machine Profile field, resource kind, Apply Policy, and verification method, and the rules publishing enforces.
A Machine Profile is the JSONC file you write on the Source Machine to
declare the configuration Follower Machines should have. canonfig source publish --profile-file <file> validates it and signs a Profile Revision,
which never changes after publication. Comments and layout in the file do not
change the Profile Revision's digest.
Profile file
{
"version": 2,
"id": "workstation",
"name": "Developer workstation",
"groups": [
{ "name": "developers", "description": "Developer tools" }
],
"resources": [],
"scheduleDefault": { "type": "daily", "at": "04:30", "timezone": "local" }
}| Field | Required | Value |
|---|---|---|
version | No | Always 2. The file format version, not a revision number. |
id | Yes | Profile ID that Follower Machines select with --profile. |
name | Yes | Display name. |
groups | No | Follower Groups: { "name", "description"? }. Resources can target only declared groups. |
resources | No | Profile Resources. Publishing an empty profile needs --allow-empty. |
scheduleDefault | No | A suggested schedule; see Suggested schedule. |
IDs and group names start with a letter or digit and contain only letters,
digits, ., _, :, and -.
Publication assigns each revision the next sequence number. Publishing content equal to the latest revision returns that revision unchanged.
Suggested schedule
scheduleDefault is a suggestion. Sync never installs it. A Follower Machine
that has no schedule set sees "schedule available" and accepts it with
canonfig schedule set --default.
| Shape | Example |
|---|---|
| Daily | { "type": "daily", "at": "04:30", "timezone": "local" } |
| Weekly | { "type": "weekly", "days": ["Mon", "Thu"], "at": "12:30", "timezone": "local" } |
timezone must be "local": each Follower Machine runs at that time in its
own time zone. A Follower Machine that needs a named time zone sets its own
schedule with canonfig schedule set --timezone. See
Sync on a schedule.
Resource fields
| Field | Required | Value |
|---|---|---|
id | Yes | Unique within the profile. |
kind | Yes | file, directory, config, skill, tool, or credential. |
target | Yes | Path on the Follower Machine (~ is the Follower Machine's home directory), or the command name for a tool. No .. segments or glob characters. |
policy | No | Apply Policy. Defaults by kind; see below. |
groups | No | Apply only on Follower Machines in one of these Follower Groups. |
dependsOn | No | Resource IDs that must apply first. No cycles. |
spec | Yes | Kind-specific content. Its kind matches the resource kind. |
verify | Yes | How Canonfig checks the result; see Verification. |
Publication refuses duplicate IDs, missing dependencies, dependency cycles, undeclared groups, invalid targets, and a policy or verification method the kind does not allow.
Resource kinds and defaults
| Kind | What a Follower Machine gets | Default policy | Other allowed policies |
|---|---|---|---|
file | One file with exact content and mode, or a symlink | replace | replace-if-unmodified, append-local |
directory | A tree of files and directories | mirror-owned | replace |
config | Declared keys inside a JSON, TOML, or YAML file | merge | replace |
skill | A skill directory | replace-if-unmodified | replace |
tool | An installed, verified command-line tool | ensure | none |
credential | A check that a local credential exists | require-local | none |
Author file, config, and skill resources
canonfig source scan proposes only tool resources. Write every other
resource in the profile file. Sync your first two machines
builds a complete Claude Code and Codex profile from these shapes.
file
{
"id": "claude-instructions",
"kind": "file",
"target": "~/.claude/CLAUDE.md",
"spec": { "kind": "file", "source": ".claude/CLAUDE.md" },
"verify": { "method": "digest" }
}{
"id": "review-helper",
"kind": "file",
"target": "~/.local/bin/review-helper",
"spec": { "kind": "file", "content": "#!/bin/sh\ngit diff --stat\n", "executable": true },
"verify": { "method": "digest" }
}| Spec field | Value |
|---|---|
source | Path relative to the profile file's directory. See Source paths. |
content | Inline UTF-8 text. Use either source or content, not both. |
encoding | "base64" when content is base64. Only with content. |
mode | Decimal permission bits, for example 384 for 0600. |
executable | true writes mode 0700 when mode is not set. |
symlinkTo | Write a symlink with this exact target instead of a file. Verify it with symlink. |
Without mode, Canonfig writes a regular file with mode 0600, or 0700 when
executable is true. Binary files work the same way: point source at them.
config
{
"id": "codex-config",
"kind": "config",
"target": "~/.codex/config.toml",
"spec": {
"kind": "config",
"format": "toml",
"keys": [
{ "path": "model_reasoning_effort", "value": "high" },
{ "path": "mcp_servers.memory", "value": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-memory"] } }
]
},
"verify": { "method": "digest" }
}format is json, toml, or yaml. Each path is a dotted key, and the
resource owns the whole value at that key; see
Structured configuration ownership.
Canonfig writes the target with mode 0600 and creates missing parent
directories with mode 0700.
skill and directory
{
"id": "claude-review-skill",
"kind": "skill",
"target": "~/.claude/skills/code-review",
"spec": {
"kind": "skill",
"name": "code-review",
"files": [
{ "path": "SKILL.md", "source": ".claude/skills/code-review/SKILL.md" },
{ "path": "scripts/check.sh", "source": ".claude/skills/code-review/scripts/check.sh", "executable": true }
]
},
"verify": { "method": "digest" }
}A skill needs name; a directory does not. Both list every file in
files. Each file has a path inside the tree and the same fields as a
file spec. Both also accept:
| Spec field | Value |
|---|---|
mode | Decimal mode of the root directory. |
directories | Directories to create even when empty: { "path", "mode" }, both required. |
{
"id": "local-tools",
"kind": "directory",
"target": "~/.local/tools",
"spec": {
"kind": "directory",
"mode": 493, // 0755
"directories": [{ "path": "cache", "mode": 488 }], // 0750
"files": [{ "path": "current", "symlinkTo": "../bin/rg" }]
},
"verify": { "method": "digest" }
}A symlink target is written exactly as authored, so a relative target stays relative to the link.
tool and credential
A tool spec has toolId, recipes, and optional login and agentInstall.
See How Canonfig installs and verifies tools.
agentInstall lets a Configuration Agent install the tool when no recipe fits:
"agentInstall": {
"paths": ["~/.local/share/canonfig/tools/review-cli"],
"origins": ["https://registry.npmjs.org"]
}paths (at least one) are where the agent may write, and origins are exact
HTTPS origins it may download from. Without agentInstall, a tool with no
usable recipe needs Human Action Required.
A credential spec has one field, reference: the name of a credential each
Follower Machine must already hold. The value never leaves the Follower
Machine.
Source paths
A source is resolved from the directory that contains the profile file. It
must be relative, must not contain .., and must resolve (after following
symlinks) inside that directory. A source that is a symlink to a file inside
the directory is published as a symlink. For a link that points elsewhere, use
symlinkTo. Anything else fails before signing, for example:
resource claude-instructions cannot publish source ../outside.txt: ".." segments are not allowed; keep the source inside the profile file's directory
If the profile file sits in your home directory, sources can point at the real
files, and an edit followed by canonfig source publish is the whole update.
See Publish a change to your Follower Machines.
Size limit
A revision's metadata (every resource, file entry, and setting, but not file
content) must fit in 1 MiB. A directory with many small files can exceed it.
canonfig source digest and canonfig source publish refuse an oversized
profile with exit 2. Split large resources across separate profiles.
Apply Policies
| Policy | Behavior |
|---|---|
replace | Writes the desired content over the target. On a directory, also removes entries Canonfig never wrote, so the tree matches exactly. |
mirror-owned | Adds and updates desired files, and removes only files Canonfig wrote earlier and nobody changed. Other entries stay. |
merge | Writes declared keys and keeps everything else in the file, including the Local Overlay. Removes a key Canonfig owned that the profile dropped, unless a Local Overlay claims it. |
replace-if-unmodified | Writes only when the target still matches what Canonfig last applied, or already matches the profile. A missing target is recreated. A changed target is Follower Drift: kept, reported, and the run stops with exit 4. |
append-local | Keeps the Source Machine's text in a marked section at the top of the file and the Follower Machine's text below it. |
ensure | Installs and verifies a tool. Never removes other software. |
require-local | Checks that a local credential exists. Otherwise Human Action Required. |
Parent directories that desired files need are created even when the profile does not list them. They never count as drift.
Keep local instructions with append-local
Use append-local on a file such as AGENTS.md when each Follower Machine
adds its own instructions. The file must use inline content (not source),
must not be executable or a symlink, and the content must not contain the
markers below or NUL characters. The file on the Follower Machine looks like
this:
<!-- canonfig:source:start -->
Source instructions
<!-- canonfig:source:end -->
Follower-local instructions- Write local instructions below the closing marker. Later syncs replace only the Source Machine's section and keep local text exactly, including whitespace and line endings.
- On first adoption, the whole existing file becomes local text, even if it matches the Source Machine's text. The Follower Machine then holds the Source Machine's section plus its old copy. Canonfig does not guess which old lines to drop. Back up the file before you publish the policy.
- Editing or deleting the Source Machine's section is Follower Drift. Move the edit below the closing marker. Repeated or broken markers need manual repair. Deleting the whole file lets Canonfig recreate it.
- Removing the resource from the profile removes only an unchanged Source Machine section and leaves local text.
To see exactly what first adoption will write, run this on the Follower Machine:
canonfig sync --plan --review-localIt writes a JSON review for each first adoption to an adoption-review-<id>
directory under the user data directory's canonfig folder, readable only by
you. sourceAddition is the Source Machine's text, retainedLocal is the local text, and
proposedText is the complete file apply will write. If either file changes
before you apply, review again.
Reviews can contain secrets
A review holds the Follower Machine's local text. Do not paste it into chat or bug reports. Delete the review directory after you read it.
--review-local works only with an interactive --plan; it cannot be combined
with --apply, --no-input, or --scheduled.
Verification
Every resource declares verify. Canonfig runs it after every change,
including changes made by an agent, and only a passing verification counts.
| Method | Fields | Allowed on |
|---|---|---|
digest | digest (optional) | directory, config, skill, and regular file |
symlink | target, equal to symlinkTo | file with symlinkTo |
command | command (argument list), expectContains, proves | directory, config, skill, tool, credential |
executable-present | executable | tool |
credential-present | reference | credential |
mcp-qualification | MCP server probes | tool; see MCP qualification |
A regular file uses digest only.
Digests
Write { "method": "digest" } without a value, and publication computes the
digest from the content it signs. If you write a digest and it does not
match, publication fails before signing:
the profile cannot be published: resource codex-config declares verify.digest
<declared>, but its published content has digest<computed>. Set verify.digest to<computed>, or omit it and publish computes it (canonfig source digestprints it)
To print the computed digests without publishing:
canonfig source digest --profile-file ~/canonfig.profile.jsonc
canonfig source digest --profile-file ~/canonfig.profile.jsonc --resource codex-configsource digest exits 0 even when a declared digest differs.
Commands
A command verifier passes when the process exits 0 and, if set, its output
contains expectContains. A bare executable name is looked up as described in
Verification lookup.
Set "proves": "client-load" only when the command starts the real client and
makes it load the applied configuration. canonfig status reports that
evidence separately:
{
"method": "command",
"command": ["claude", "mcp", "list"],
"proves": "client-load"
}Applied Resource Records
After applying a resource, the Follower Machine records exactly what it wrote:
the Profile Revision, digest, modes, symlink targets, and files it owns.
Canonfig compares the Follower Machine with this record to detect Follower
Drift and to decide which files mirror-owned may remove. The record never
gives the Follower Machine authority over the profile.
Structured configuration ownership
A config value can be a string, number, boolean, array, or nested object.
null is not allowed, because TOML cannot represent it. Arrays keep their
order.
Numbers must be finite and, when whole, between -9007199254740991 and 9007199254740991. Canonfig never rounds; a larger number fails publication:
the number at $.resources[0].spec.keys[0].value.big cannot be carried exactly
Write such a value as a string if the consuming tool accepts one.
Each path owns the entire value at that key. A profile cannot declare both
mcpServers.memory and mcpServers.memory.command, or the same path twice.
A Local Overlay key conflicts with a profile key when one contains the other;
the resource then needs Human Action Required until you change the overlay.
Escape a dot inside a key name as \., and a backslash as \\. In JSON each
backslash is itself escaped, so the key amp.mcpServers → docs is:
{ "path": "amp\\.mcpServers.docs", "value": { "command": "docs-server" } }Any other backslash is rejected by publication and by canonfig overlay set.
How Canonfig edits files on the Follower Machine
- JSON targets may contain
//and/* */comments and trailing commas. Canonfig edits only the owned keys, so comments and layout elsewhere stay. - TOML and YAML files are edited in place. Comments, key order, anchors, and
value spellings outside owned keys stay. A whole number replacing a float
stays a float (
2.0). - An owned value equal to the profile's value in a different key order is left alone. A changed value is written with sorted keys. Unmanaged keys are never reordered.
- In YAML, strings such as
yes,no,on,off,~, and2001-12-14are written in double quotes so other readers do not convert them. - If a change cannot be written without changing values outside the owned keys (for example a key under a shared YAML anchor), the action fails and the file is unchanged.
- A file on the Follower Machine that does not parse blocks the resources that target it.
canonfig sync --planshows each ashuman-actionwith the file, line, and column. Repair the file, then runcanonfig sync --apply.
When you narrow ownership from an object to some of its keys, the other keys
stay in the file on the Follower Machine. When you widen ownership to the parent, the profile
value replaces the whole object. The plan lists kept values under retains.
Compile a profile from code
To check a profile in your own scripts, import the compiler that publication uses:
import { compileProfileJsonc } from "@microck/canonfig/profile-compiler";
const candidate = compileProfileJsonc(profileJsonc);candidate.digest and candidate.canonicalBytes are what publication would
sign. candidate.projections holds the Linux, macOS, and Windows versions of
the profile, each with only that platform's tool recipes. Pass
{ platforms: ["linux"] } to compile fewer platforms. An invalid profile
throws before returning anything.