openclaw path CLI Reference: Inspect and Edit Workspace Files via oc://
This page covers the openclaw path command for inspecting and modifying workspace files using the oc:// addressing scheme. It is essential for self-hosters, plugin authors, and editor extensions who need to read or update specific file locations without custom parsers.
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
Shell access to the oc:// addressing model uses a single path syntax that dispatches by file type. It lets you inspect and modify addressable workspace files, including markdown, jsonc, jsonl, yaml, yml, and lobster. Self-hosters, plugin authors, and editor extensions rely on it to read, locate, or update a specific location without writing a custom parser for each file format.
The bundled optional oc-path plugin provides path. Activate it before using it:
openclaw plugins enable oc-path
The CLI verbs follow the addressing model:
resolvetargets a single, concrete match.findhandles multiple matches, supporting wildcards, unions, predicates, and positional expansion.setaccepts only concrete paths or insertion markers; wildcard patterns cause rejection before any write.validateparses a path without accessing the filesystem.emitruns a file through parse and emit in a round trip, providing a byte-fidelity diagnostic.
Why use it
OpenClaw state lives across human-edited markdown, commented JSONC configuration, append-only JSONL logs, and YAML workflow or spec files. Scripts, hooks, and agents frequently need a single small value from those files: a frontmatter key, a plugin setting, a log record field, a YAML step, or a bullet item under a named section.
openclaw path gives those callers a stable address instead of a one-off grep, regex, or per-file parser. 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 remains untouched, so writing one leaf does not disturb its comments, line endings, or nearby formatting.
Use it when what you need has a logical address but the file shape varies:
- A hook reads one setting from commented JSONC and writes the value back without losing comments.
- A maintenance script finds every matching event field in a JSONL log without loading the entire 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, with the changed bytes visible during review.
Skip openclaw path for ordinary whole-file edits, complex config migrations, or memory-specific writes; use the owner command or plugin for those. path is for small, addressable file operations where a repeatable terminal command beats another bespoke parser.
How it is used
Read one 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 instead of 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 copyable into shell scripts. Use --json when a caller needs structured output and --human when a person is inspecting the result.
How it works
- Parses the
oc://address into slots: file, section, item, field, and an optional session query. - Selects 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 and items, JSONC object keys and array indexes, JSONL line records, or YAML map and sequence nodes.
- For
set, emits edited bytes through the same adapter so untouched parts of the file keep their comments, line endings, and nearby formatting where the kind supports it.
resolve and set require one concrete target. find is the exploratory verb: it expands wildcards, unions, predicates, and ordinals into the concrete matches you can inspect before choosing 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 | Determine the file slot relative to this directory (default: process.cwd()). |
--file <path> | resolve, find, set, emit | Replace the resolved file slot path with an absolute one. |
--json | all | Enforce JSON output (default when stdout is not a TTY). |
--human | all | Enforce human output (default when stdout is a TTY). |
--value-json | set | Treat <value> as JSON for JSON/JSONC/JSONL leaf substitution. |
--dry-run | set | Show the bytes that would be written without performing the write. |
--diff | set (requires --dry-run) | Display a unified diff rather than the complete bytes. |
validate accepts only --json / --human; it performs no filesystem operations, so
--cwd and --file are irrelevant.
oc:// syntax
oc://FILE/SECTION/ITEM/FIELD?session=SCOPE
Slot constraints: field demands item, and item demands section. For
all four slots:
- Quoted segments, The content between
"a/b.c"survives/and.separators. Inside quotes, everything is treated as literal bytes;"and\are forbidden. The file slot also respects quoting:oc://"skills/email-drafter"/Tools/$lastinterpretsskills/email-drafteras a single file path. - Predicates, Available predicates include
[k=v],[k!=v],[k<v],[k<=v],[k>v], and[k>=v]. When using numeric operators, both operands must be coercible to finite numbers. - Unions,
{a,b,c}matches any one of the given alternatives. - Wildcards, Use
*for a single sub-segment and**for zero or more recursive matches.findsupports these;resolveandsetreject them due to ambiguity. - Positional,
$firstand$lastresolve to the first or last index, or to a declared key. - Ordinal,
#Nselects the Nth match according to document order. - Insertion markers, For keyed or indexed insertion, use
+,+key, or+nnn(in combination withset). - Session scope,
?session=cron-dailyand similar markers operate independently of slot nesting. Session values are stored raw, not percent-decoded, and must not 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 rejected everywhere, including inside the session query value.
formatOcPath(parseOcPath(path)) === path is guaranteed for canonical paths. Non-canonical query parameters are ignored, except the first non-empty session= value.
Hard limits apply: 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. Separately, any JSONC or JSON file larger than 16 MiB is rejected with a parse diagnostic rather than being parsed, for any verb that loads that file.
Addressing by file kind
| Kind | File extensions | Addressing model |
|---|---|---|
| Markdown | .md | H2 sections by slug, bullet items by slug or #N, frontmatter accessed 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 returns a structured match: one of root, node, leaf, or insertion-point, along with a 1-based line number. Leaf values are presented as text plus a leafType, enabling plugin authors to render previews without depending on the per-kind AST shape.
Mutation contract
set writes a single concrete target.
- String values are leaf nodes in Markdown frontmatter and
- key: valueitem fields. Markdown insertions append sections, frontmatter keys, or section items, then produce a canonical markdown structure for the modified file. Writing section bodies as a whole throughsetis not supported. - For JSONC leaf writes, the string value is coerced to match the existing leaf type (
string, finitenumber,true/false, ornull). When a JSONC/JSON/JSONL leaf replacement should interpret<value>as JSON and may alter shape, for instance swapping a string secret-ref shorthand for an object, use--value-json. JSONC object and array insertions parse<value>as JSON and apply thejsonc-parseredit path for ordinary leaf writes, keeping comments and nearby formatting intact. - JSONL leaf writes coerce the same way as JSONC within a line. Whole-line replacement and append parse
<value>as JSON. The rendered JSONL preserves the file's dominant line-ending convention (LF or CRLF determined by majority vote across the file's newlines, so a mostly-CRLF file stays CRLF even with a few stray LFs). - YAML leaf writes coerce to the existing scalar type (
string, finitenumber,true/false, ornull). YAML insertions rely on the bundledyamlpackage's document API for map/sequence updates. Malformed YAML documents with parser errors are rejected before mutation usingparse-error.
When exact byte fidelity matters, use --dry-run before user-visible writes. 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 any edit, which can normalize incidental formatting outside the changed leaf. Add --diff when you want the preview as a focused before/after patch instead of 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 operate across kinds; the addressing scheme dispatches based 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 addresses the YAML frontmatter block; tools matches the ## Tools heading via slug, and item leaves retain their slug form even when the source uses underscores (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 go through jsonc-parser, so comments and whitespace survive a set. Run with --dry-run first to inspect the bytes before committing. .json files use 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 is a record. Address by predicate ([event=action]) when you do not know the line number, or by the canonical LN segment when you do. .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 rather than a hand-rolled parser, so ordinary parse/emit round-trips preserve comments and authoring shape while resolved paths use the same map-key / sequence-index model as JSONC. The same adapter handles .yaml, .yml, and .lobster files.
Subcommand reference
resolve <oc-path>
Read a single leaf or node. Wildcards are not accepted; use find instead.
Returns 0 when a match is found, 1 if nothing matches, and 2 for a parse error or a pattern that was refused.
openclaw path resolve 'oc://AGENTS.md/tools/gh/risk' --human
openclaw path resolve 'oc://gateway.jsonc/server/port' --json
find <pattern>
List all matches for a wildcard, predicate, or union pattern. Returns 0 when at least one match exists, 1 when none are found. File-slot wildcards are refused with OC_PATH_FILE_WILDCARD_UNSUPPORTED; supply a concrete file instead (multi-file globbing will come in a later release).
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>
Write a single leaf. Use --dry-run to preview the bytes that would be written without modifying the file. Add --diff to see a unified diff preview. Returns 0 on a successful write, 1 if the substrate refuses the operation (for example, when a sentinel guard triggers), and 2 on parse errors.
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 +key insertion marker creates the named child if it does not already exist; +nnn and bare + handle indexed and append insertion respectively.
validate <oc-path>
Parse-only check. Does not access the filesystem. Useful for verifying that a template path is well-formed before variable substitution, or for getting 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
Returns 0 when valid, 1 when invalid (along with a structured code and message), and 2 on argument errors.
emit <file>
Round-trip a file through its per-kind parser and emitter. On a valid file, the output should match the input byte for byte; any difference points to a parser bug or a sentinel trigger. Useful for debugging substrate behavior on real-world inputs.
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 succeeded.) |
1 | No match, or set rejected by the substrate (no system-level error). |
2 | Argument or parse error. |
Output mode
openclaw path adapts to the terminal: human-readable output when connected to a TTY, JSON when stdout is piped or redirected. --json and --human override this automatic detection.
Notes
setwrites bytes through the substrate's emit path, which automatically applies the redaction-sentinel guard. A leaf containing__OPENCLAW_REDACTED__(either verbatim or as a substring) is rejected at write time.- JSONC parsing and leaf edits use the plugin-local
jsonc-parserdependency, so comments and formatting are preserved on ordinary leaf writes rather than being processed by a hand-written parser or re-render path. pathdoes not track last-known-good (LKG) config state or handle recovery; that lifecycle is managed elsewhere. If a file you edit throughpathis also under LKG tracking, the next config read decides whether to promote or recover it; treat apathedit like any other direct write to that file.