openclaw path CLI: Inspect and Edit Workspace Files via oc://
Reference for the openclaw path command, which lets you read, locate, or update workspace files using the oc:// addressing scheme. Useful for self-hosters, plugin authors, and editor extensions.
Read this when
- You want to read or write a leaf inside a workspace file from the terminal
- You're scripting against workspace state and want a stable, kind-agnostic addressing scheme
- You're debugging a `oc://` path (validate the syntax, see what it resolves to)
openclaw path
The oc:// addressing scheme is reachable from the shell through a single path syntax that dispatches by file kind, letting you inspect or modify addressable workspace files such as markdown, jsonc, jsonl, and yaml/yml/lobster. Self-hosters, plugin authors, and editor extensions rely on it to read, locate, or update a specific spot without building a custom parser for each file type.
The bundled optional oc-path plugin supplies path. Activate it before your first use:
openclaw plugins enable oc-path
The CLI verbs mirror the addressing model:
resolvetargets a single, specific match.findhandles multiple matches, covering wildcards, unions, predicates, and positional expansion.setrejects wildcard patterns before writing; it only accepts concrete paths or insertion markers.validateparses a path without touching the filesystem.emitruns a file through parse and emit to check byte fidelity.
Why use it
OpenClaw state lives in various human-edited formats: markdown, JSONC config with comments, append-only JSONL logs, and YAML workflow or spec files. Scripts, hooks, and agents frequently need a single value from these files, like a frontmatter key, a plugin setting, a log record field, a YAML step, or a bullet item under a named section.
openclaw path provides those callers with a stable address, replacing a one-off grep, regex, or parser per file kind. The same oc:// path can be validated, resolved, searched, dry-run, and written from the terminal, keeping narrow automation reviewable and replayable. The rest of the file stays intact, so writing one leaf does not alter its comments, line endings, or nearby formatting.
Reach for it when your target has a logical address but the file shape differs:
- A hook pulls one setting from commented JSONC and writes it back without dropping comments.
- A maintenance script locates every matching event field in a JSONL log without loading the whole log into a custom parser.
- An editor jumps to a markdown section or bullet item by slug, then renders the exact line it resolved to.
- An agent dry-runs a small workspace edit before applying it, showing the changed bytes for review.
Skip openclaw path for typical whole-file edits, rich config migrations, or memory-specific writes; use the owner command or plugin for those. path suits small, addressable file operations where a repeatable terminal command beats another bespoke parser.
How it is used
Pull a single value from a human-edited config file:
openclaw path resolve 'oc://config.jsonc/plugins/github/enabled'
Preview a write without touching disk:
openclaw path set 'oc://config.jsonc/plugins/github/enabled' 'true' --dry-run
Find matching records in an append-only JSONL log:
openclaw path find 'oc://session.jsonl/[event=tool_call]/name'
Address an instruction in markdown by section and item rather than by line number:
openclaw path resolve 'oc://AGENTS.md/runtime-safety/openclaw-gateway'
Validate a path in CI or a preflight script before the script reads or writes:
openclaw path validate 'oc://AGENTS.md/tools/$last/risk'
These commands are designed to be dropped into shell scripts. Use --json when a caller needs structured output and --human when a person is inspecting the result.
How it works
- Splits the
oc://address into slots: file, section, item, field, and an optional session query. - Picks the file-kind adapter based on the target extension (
.md,.jsonc,.json,.jsonl,.ndjson,.yaml,.yml,.lobster). - Resolves the slots against that file kind's structure: markdown headings or items, JSONC object keys or array indexes, JSONL line records, or YAML map or sequence nodes.
- For
set, emits edited bytes through the same adapter, preserving comments, line endings, and nearby formatting in untouched parts where the kind supports it.
resolve and set demand one concrete target. find is the exploratory verb: it expands wildcards, unions, predicates, and ordinals into concrete matches you can inspect before picking one to write.
Subcommands
| Subcommand | Purpose |
|---|---|
resolve <oc-path> | Print the concrete match at the path (or "not found"). |
find <pattern> | Enumerate matches for a wildcard / union / predicate path. |
set <oc-path> <value> | Write a leaf or insertion target at a concrete path. Supports --dry-run. |
validate <oc-path> | Parse-only; print the structural breakdown (file / section / item / field). |
emit <file> | Round-trip a file through parse + emit (byte-fidelity diagnostic). |
Global flags
| Flag | Applies to | Purpose |
|---|---|---|
--cwd <dir> | resolve, find, set, emit | Use this directory to resolve the file slot, falling back to process.cwd() if unset. |
--file <path> | resolve, find, set, emit | Replace the file slot's resolved path with this one, enabling direct access. |
--json | all | Always emit JSON, which is the default when stdout lacks a TTY. |
--human | all | Always emit human-readable output, the default when stdout is a TTY. |
--value-json | set | Treat <value> as JSON when substituting leaf values in JSON, JSONC, or JSONL. |
--dry-run | set | Show the bytes that would be written, without actually writing them. |
--diff | set (needs --dry-run) | Show a unified diff instead of the complete byte output. |
Only validate / --json are accepted by --human; since no filesystem interaction occurs, --cwd and --file are irrelevant.
oc:// syntax
oc://FILE/SECTION/ITEM/FIELD?session=SCOPE
Slot constraints: field depends on item, while item depends on section. Considering all four slots together:
- Quoted segments,
"a/b.c"remains intact across/and.separators. The content is treated byte-for-byte;"and\are forbidden within quotation marks. The file slot respects quotes as well:oc://"skills/email-drafter"/Tools/$lastinterpretsskills/email-drafteras one complete file path. - Predicates,
[k=v],[k!=v],[k<v],[k<=v],[k>v],[k>=v]. Both operands of numeric operators must be coercible to finite numbers. - Unions,
{a,b,c}matches any one of the listed alternatives. - Wildcards,
*covers a single sub-segment, while**handles zero-or-more recursively.findpermits these;resolveandsetrefuse them due to ambiguity. - Positional,
$first/$lastpoint to the first / last index or declared key. - Ordinal,
#Nselects the Nth match according to document order. - Insertion markers,
+,+key,+nnnfor keyed or indexed insertion, meant to be paired withset. - Session scope,
?session=cron-dailyand similar. Independent of slot nesting. Session values stay raw, without percent-decoding, and cannot contain control characters or reserved query delimiters (?,&,%).
Reserved characters (?, &, %) are disallowed outside quoted, predicate, or union segments. Control characters (U+0000-U+001F, U+007F) are disallowed everywhere, including the session query value.
formatOcPath(parseOcPath(path)) === path is assured for canonical paths. Non-canonical query parameters are disregarded, except for the initial non-empty session= value.
Fixed limits: a path is capped at 4096 bytes, with at most 4 slots (file/section/item/field), at most 64 dotted sub-segments per slot, and at most 256 nested traversal levels for deep JSON paths. Additionally, any file input over 16 MiB is rejected before parsing for every verb that loads the file. JSONC/JSON preserves the OC_JSONC_INPUT_TOO_LARGE diagnostic; other file types rely on OC_PATH_INPUT_TOO_LARGE.
Addressing by file kind
| Kind | File extensions | Addressing model |
|---|---|---|
| Markdown | .md | H2 sections by slug, bullet items by slug or #N, frontmatter via [frontmatter]. |
| JSONC/JSON | .jsonc, .json | Object keys and array indexes; dots split nested sub-segments unless quoted. |
| JSONL | .jsonl, .ndjson | Top-level line addresses (L1, L2, $first, $last), then JSONC-style descent inside the line. |
| YAML/.lobster | .yaml, .yml, .lobster | Map keys and sequence indexes; comments and flow style are handled by the YAML document API. |
resolve yields a structured match: root, node, leaf, or insertion-point, accompanied by a 1-based line number. Leaf values appear as text along with a leafType, letting plugin authors render previews without depending on the per-kind AST structure.
Mutation contract
set handles a single concrete target:
- Within Markdown, frontmatter values and
- key: valueitem fields act as string leaves. Insertions append sections, frontmatter keys, or section items, then emit a canonical markdown layout for the modified file. Whole section bodies cannot be written throughset. - For JSONC leaf writes, the string value is coerced to the leaf's existing
type (
string, finitenumber,true/false, ornull). When a JSONC/JSON/JSONL leaf needs replacing and the new content should parse<value>as JSON with a possible shape change, such as swapping a string secret-ref shorthand for an object, use--value-json. Object and array insertions in JSONC parse<value>as JSON, while ordinary leaf edits go through thejsonc-parseredit path, which keeps comments and surrounding formatting intact. - JSONL leaf writes coerce like JSONC but within a single line. Both
whole-line replacement and append parse
<value>as JSON. The rendered JSONL keeps the file's dominant LF/CRLF convention, determined by a majority vote across newlines, so a mostly-CRLF file stays CRLF even with stray LFs. - YAML leaf writes coerce to the existing scalar type (
string, finitenumber,true/false, ornull). Insertions rely on the bundledyamlpackage's document API for map and sequence updates. Any YAML document with parser errors is rejected before mutation, usingparse-error.
When exact bytes matter for user-visible writes, call --dry-run first. JSONC
and YAML edits patch the existing document, via jsonc-parser or the yaml
document API, so untouched bytes usually persist; markdown rebuilds the file
from its parsed structure on every edit, which can normalize incidental
formatting outside the changed leaf. Add --diff to see the preview as a
focused before/after patch rather than the full rendered file.
Examples
# Validate a path (no filesystem access)
openclaw path validate 'oc://AGENTS.md/Tools/$last/risk'
# Read a leaf
openclaw path resolve 'oc://gateway.jsonc/version'
# Wildcard search
openclaw path find 'oc://session.jsonl/*/event' --file ./logs/session.jsonl
# Dry-run a write
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run
# Dry-run a write as a unified diff
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run --diff
# Apply the write
openclaw path set 'oc://gateway.jsonc/version' '2.0'
# Byte-fidelity round-trip (diagnostic)
openclaw path emit ./AGENTS.md
Additional grammar examples:
# Quote keys containing / or .
openclaw path resolve 'oc://config.jsonc/agents.defaults.models/"anthropic/claude-opus-4-7"/alias'
# Deep JSON/JSONC paths can use slash segments; they normalize to dotted subsegments
openclaw path set 'oc://openclaw.json/agents/list/0/tools/exec/security' 'allowlist' --dry-run
# Replace a JSONC leaf with a parsed object
openclaw path set 'oc://openclaw.json/gateway/auth/token' '{"source":"file","provider":"secrets","id":"/test"}' --value-json --dry-run
# Predicate search over JSONC children
openclaw path find 'oc://config.jsonc/plugins/[enabled=true]/id'
# Insert into a JSONC array
openclaw path set 'oc://config.jsonc/items/+1' '{"id":"new","enabled":true}' --dry-run
# Insert a JSONC object key
openclaw path set 'oc://config.jsonc/plugins/+github' '{"enabled":true}' --dry-run
# Append a JSONL event
openclaw path set 'oc://session.jsonl/+' '{"event":"checkpoint","ok":true}' --file ./logs/session.jsonl
# Resolve the last JSONL value line
openclaw path resolve 'oc://session.jsonl/$last/event' --file ./logs/session.jsonl
# Resolve a YAML workflow step
openclaw path resolve 'oc://workflow.yaml/steps/0/id'
# Update a YAML scalar
openclaw path set 'oc://workflow.yaml/steps/$last/id' 'classify-renamed' --dry-run
# Address markdown frontmatter
openclaw path resolve 'oc://AGENTS.md/[frontmatter]/name'
# Insert markdown frontmatter
openclaw path set 'oc://AGENTS.md/[frontmatter]/+description' 'Agent instructions' --dry-run
# Find markdown item fields
openclaw path find 'oc://SKILL.md/Tools/*/send_email'
# Validate a session-scoped path
openclaw path validate 'oc://AGENTS.md/Tools/$last/risk?session=cron-daily'
Recipes by file kind
The same five verbs apply across kinds; the addressing scheme dispatches on the file extension.
Markdown
<!-- frontmatter.md -->
---
name: drafter
description: email drafting agent
tier: core
---
## Tools
- gh: GitHub CLI
- curl: HTTP client
- send_email: enabled
$ openclaw path resolve 'oc://x.md/[frontmatter]/tier' --file frontmatter.md --human
leaf @ L4: "core" (string)
$ openclaw path resolve 'oc://x.md/tools/gh/gh' --file frontmatter.md --human
leaf @ L9: "GitHub CLI" (string)
$ openclaw path find 'oc://x.md/tools/*' --file frontmatter.md --human
3 matches for oc://x.md/tools/*:
oc://x.md/tools/gh → node @ L9 [md-item]
oc://x.md/tools/curl → node @ L10 [md-item]
oc://x.md/tools/send-email → node @ L11 [md-item]
The [frontmatter] predicate targets the YAML frontmatter block; tools matches
the ## Tools heading by slug, and item leaves retain their slug form even
when the source uses underscores, so send_email becomes send-email.
JSONC
// config.jsonc
{
"plugins": {
"github": {"enabled": true, "role": "vcs"},
"slack": {"enabled": false, "role": "chat"}
}
}
$ openclaw path resolve 'oc://config.jsonc/plugins/github/enabled' --file config.jsonc --human
leaf @ L4: "true" (boolean)
$ openclaw path set 'oc://config.jsonc/plugins/slack/enabled' 'true' --file config.jsonc --dry-run
--dry-run: would write 142 bytes to /…/config.jsonc
{
"plugins": {
"github": {"enabled": true, "role": "vcs"},
"slack": {"enabled": true, "role": "chat"}
}
}
JSONC edits flow through jsonc-parser, so comments and whitespace survive a
set. Run with --dry-run first to check the bytes before committing.
.json files share the same adapter and edit path as .jsonc.
JSONL
{"event":"start","userId":"u1","ts":1}
{"event":"action","userId":"u1","ts":2}
{"event":"end","userId":"u1","ts":3}
$ openclaw path find 'oc://session.jsonl/[event=action]/userId' --file session.jsonl --human
1 match for oc://session.jsonl/[event=action]/userId:
oc://session.jsonl/L2/userId → leaf @ L2: "u1" (string)
$ openclaw path resolve 'oc://session.jsonl/L2/ts' --file session.jsonl --human
leaf @ L2: "2" (number)
Each line counts as a record. Address by predicate ([event=action]) when the line
number is unknown, or by the canonical LN segment when it is known.
.ndjson files use the same adapter as .jsonl.
YAML
# workflow.yaml
name: inbox-triage
steps:
- id: fetch
command: gmail.search
- id: classify
command: openclaw.invoke
$ openclaw path resolve 'oc://workflow.yaml/steps/0/id' --file workflow.yaml --human
leaf @ L3: "fetch" (string)
$ openclaw path set 'oc://workflow.yaml/steps/$last/id' 'classify-renamed' --file workflow.yaml --dry-run
--dry-run: would write 99 bytes to /…/workflow.yaml
name: inbox-triage
steps:
- id: fetch
command: gmail.search
- id: classify-renamed
command: openclaw.invoke
YAML uses the yaml package's Document API instead of a hand-rolled
parser, so ordinary parse/emit round-trips preserve comments and authoring
shape, while resolved paths follow the same map-key / sequence-index model
as JSONC. The same adapter handles .yaml, .yml, and .lobster files.
Subcommand reference
resolve <oc-path>
Fetch a single leaf or node. Wildcards are not accepted; rely on find for those. The command terminates with 0 when a match is found, 1 when nothing matches cleanly, and 2 when parsing fails or the pattern is refused.
openclaw path resolve 'oc://AGENTS.md/tools/gh/risk' --human
openclaw path resolve 'oc://gateway.jsonc/server/port' --json
find <pattern>
List every match produced by a wildcard, predicate, or union pattern. It exits with 0 if at least one match exists, or 1 when there are none. File-slot wildcards trigger a rejection with OC_PATH_FILE_WILDCARD_UNSUPPORTED; supply a concrete file instead, since multi-file globbing is planned as a later enhancement.
openclaw path find 'oc://AGENTS.md/tools/**/risk'
openclaw path find 'oc://session.jsonl/[event=action]/userId'
openclaw path find 'oc://config.jsonc/plugins/{github,slack}/enabled'
set <oc-path> <value>
Set a leaf's value. Combine this with --dry-run to see the exact bytes that would be written without modifying the file. Adding --diff yields a unified diff preview. The exit code is 0 for a successful write, 1 when the substrate declines, such as when a sentinel guard is triggered, and 2 for parse failures.
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run --diff
openclaw path set 'oc://gateway.jsonc/version' '2.0'
openclaw path set 'oc://AGENTS.md/Tools/+gh/risk' 'low'
The insertion marker +key creates the named child when it is absent; +nnn handles indexed insertion, and bare + covers append insertion.
validate <oc-path>
Validation only, without touching the filesystem. This is handy for verifying that a template path is properly formed before substituting variables, or for obtaining a structural breakdown during debugging:
$ openclaw path validate 'oc://AGENTS.md/tools/gh' --human
valid: oc://AGENTS.md/tools/gh
file: AGENTS.md
section: tools
item: gh
It exits with 0 on valid input, 1 on invalid input (accompanied by a structured code and message), and 2 when arguments are malformed.
emit <file>
Pass a file through the type-specific parser and emitter in a round trip. On a healthy file, the output should match the input byte for byte; any divergence points to a parser defect or a sentinel encounter. This is valuable for investigating substrate behavior with realistic data.
openclaw path emit ./AGENTS.md
openclaw path emit ./gateway.jsonc --json
Exit codes
| Code | Meaning |
|---|---|
0 | Success. (resolve / find: at least one match. set: write completed.) |
1 | No match, or set declined by the substrate (no system-level error). |
2 | Argument or parse error. |
Output mode
openclaw path adapts to the terminal: it prints human-friendly output on a TTY and switches to JSON when stdout is piped or redirected. Use --json and --human to force either behavior.
Notes
setsends bytes through the substrate's emit path, which automatically enforces the redaction-sentinel guard. A leaf containing__OPENCLAW_REDACTED__(exactly or as part of a larger string) is blocked at write time.- JSONC parsing and leaf modifications depend on the plugin-local
jsonc-parserlibrary, so comments and formatting survive ordinary leaf writes instead of being reprocessed by a custom parser and re-renderer. pathhas no knowledge of last-known-good (LKG) config tracking or recovery; that lifecycle is managed elsewhere. When a file you edit viapathis also under LKG tracking, the next config read determines whether it gets promoted or recovered; treat apathedit like any other direct write to that file.