On this page

On this page

CLI commands

Path

openclaw path

Shell access to the oc:// addressing scheme: one kind-dispatched path syntax for inspecting and editing addressable workspace files (markdown, jsonc, jsonl, yaml/yml/lobster). Self-hosters, plugin authors, and editor extensions use it to read, find, or update a narrow location without hand-rolling a per-file parser.

path is provided by the bundled optional oc-path plugin. Enable it before first use:

bash
openclaw plugins enable oc-path

The CLI verbs mirror the addressing model:

  • resolve is concrete and single-match.
  • find is the multi-match verb for wildcards, unions, predicates, and positional expansion.
  • set only accepts concrete paths or insertion markers; wildcard patterns are rejected before writing.
  • validate parses a path with no filesystem access.
  • emit round-trips a file through parse + emit (byte-fidelity diagnostic).

Why use it

OpenClaw state is spread across human-edited markdown, commented JSONC config, append-only JSONL logs, and YAML workflow/spec files. Scripts, hooks, and agents often need one 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 parser per file kind. The same oc:// path can be validated, resolved, searched, dry-run, and written from the terminal, which keeps narrow automation reviewable and replayable. It preserves the rest of the file, so writing one leaf does not disturb its comments, line endings, or nearby formatting. YAML edits also retain an existing UTF-8 byte order mark (BOM).

Use it when the thing you want has a logical address, but the file shape varies:

  • A hook reads one setting from commented JSONC without losing comments when it writes the value back.
  • A maintenance script finds 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, with the changed bytes visible in review.

Skip openclaw path for ordinary whole-file edits, rich config migrations, or memory-specific writes; those should use the owner command or plugin. 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:

bash
openclaw path resolve 'oc://config.jsonc/plugins/github/enabled'

Preview a write without touching disk:

bash
openclaw path set 'oc://config.jsonc/plugins/github/enabled' 'true' --dry-run

Find matching records in an append-only JSONL log:

bash
openclaw path find 'oc://session.jsonl/[event=tool_call]/name'

Address an instruction in markdown by section and item instead of by line number:

bash
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:

bash
openclaw path validate 'oc://AGENTS.md/tools/$last/risk'

These commands are meant 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

  1. Parses the oc:// address into slots: file, section, item, field, and an optional session query.
  2. Chooses the file-kind adapter from the target extension (.md, .jsonc, .json, .jsonl, .ndjson, .yaml, .yml, .lobster).
  3. Resolves the slots against that file kind's structure: markdown headings/items, JSONC object keys/array indexes, JSONL line records, or YAML map/sequence nodes.
  4. 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).

When updating an existing YAML numeric field, set accepts finite decimal values, including scientific notation such as 1e-7 or 2.5E+3. You can write the numeric valueText returned by resolve back to the same field. Malformed or non-finite numeric values are rejected without changing the file.

Global flags

Flag Applies to Purpose
--cwd <dir> resolve, find, set, emit Resolve the file slot against this directory (default: process.cwd()).
--file <path> resolve, find, set, emit Override the file slot's resolved path (absolute access).
--json all Force JSON output (default when stdout is not a TTY).
--human all Force human output (default when stdout is a TTY).
--value-json set Parse <value> as JSON for JSON/JSONC/JSONL leaf replacement.
--dry-run set Print the bytes that would be written without writing.
--diff set (requires --dry-run) Print a unified diff instead of the full bytes.

validate takes only --json / --human; it does no filesystem access, so --cwd and --file do not apply.

oc:// syntax

text
oc://FILE/SECTION/ITEM/FIELD?session=SCOPE

Slot rules: field requires item, and item requires section. Across all four slots:

  • Quoted segments — "a/b.c" survives / and . separators. Quoting makes special tokens literal keys, including "$first", "*", "+key", and "[k=v]". Inside quotes, use \" for a literal quote and \\ for a literal backslash; other backslash escapes are rejected. The file slot is also quote-aware: oc://"skills/email-drafter"/Tools/$last treats skills/email-drafter as a single file path. Use "" for an empty-string object key in JSONC, JSONL, or YAML, for example oc://sample.jsonc/""/value. It is distinct from a missing segment and is not an array index.
  • Predicates — [k=v], [k!=v], [k<v], [k<=v], [k>v], [k>=v]. Numeric operators require both sides to coerce to finite numbers.
  • Unions — {a,b,c} matches any of the alternatives.
  • Wildcards — * (single sub-segment) and ** (zero-or-more, recursive). find accepts these; resolve and set reject them as ambiguous.
  • Positional — $first / $last resolve to the first / last index or declared key. Quote them ("$first" / "$last") to address literal keys with those names.
  • Ordinal — #N for the Nth match by document order.
  • Insertion markers — +, +key, +nnn for keyed / indexed insertion (use with set).
  • Session scope — ?session=cron-daily etc. Orthogonal to slot nesting. Session values are raw, not percent-decoded; they may not contain control characters or reserved query delimiters (?, &, %).

Reserved characters (?, &, %) outside quoted, predicate, or union segments are rejected. Control characters (U+0000-U+001F, U+007F) are rejected anywhere, including the session query value.

formatOcPath(parseOcPath(path)) === path is guaranteed for canonical paths. Other query parameters are ignored except for the first non-empty session= value.

File names, literal keys, and session values preserve their Unicode code points; canonically equivalent spellings remain distinct. Markdown section and item addresses still use their existing slug rules.

Hard limits: a path caps at 4096 bytes, 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, every file input over 16 MiB is refused before parsing for any verb that loads the file. JSONC/JSON keeps the OC_JSONC_INPUT_TOO_LARGE diagnostic; other file kinds use 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.

Markdown bullet fields and frontmatter keys are terminal: name.absent cannot match a name field or key. For concrete frontmatter writes, name/absent is likewise unresolved. Insert keys directly into [frontmatter] with +key; insertion below a key, such as name/+added, is not writable. Rejected writes leave the file unchanged. Recursive wildcards may consume zero segments, so name.** and **.name can still match it. A quoted "**" addresses a literal field key. Union alternatives keep their selector semantics: {missing,*} and {missing,[name=value]} can select a matching bullet field. For JSONL, ** can span both the physical line and nested value keys: oc://session.jsonl/**/event finds nested event values inside each line.

resolve returns a structured match: root, node, leaf, or insertion-point, with a 1-based line number. Leaf values are surfaced as text plus a leafType so plugin authors can render previews without depending on the per-kind AST shape.

Markdown frontmatter string reads decode YAML quoting and inline comments: key: value # note resolves to value. A quoted # or adjacent hash such as a#b remains part of the string, and non-string scalar spelling such as 00123 is preserved. Reads leave the source bytes unchanged. Block-style lists and maps resolve to YAML text, and multi-line string scalars retain YAML folding and chomping. Cross-key aliases remain alias text.

Mutation contract

set writes one concrete target:

  • Markdown frontmatter values and - key: value item fields are string leaves. Item field replacements must contain non-whitespace text; blank replacements are rejected without writing. Frontmatter values may be empty. Values are literal, including $1, $&, and $$. Markdown insertions append sections, frontmatter keys, or section items. New frontmatter keys must start with an ASCII letter or underscore and contain only ASCII letters, digits, underscores, or hyphens; unsupported keys are rejected before writing. Edits replace only the selected source range: body edits preserve the original frontmatter, and frontmatter edits preserve the body, including comments, quoting, blank lines, CRLF, and a missing final newline. Frontmatter uses --- delimiters; +++ is ordinary body text. Section bodies are not writable as a whole through set.
  • JSONC leaf writes coerce the string value to the existing leaf type (string, finite number, true/false, or null). Blank numeric replacements are rejected; use 0 to write zero. Use --value-json when a JSONC/JSON/JSONL leaf replacement should parse <value> as JSON and may change shape, such as replacing a string secret-ref shorthand with an object. JSONC object and array insertions parse <value> as JSON and use the jsonc-parser edit path for ordinary leaf writes, preserving comments and nearby formatting.
  • JSONL leaf writes coerce like JSONC inside a line. Whole-line replacement and append parse <value> as JSON. Edits replace only the selected value's source text, preserving untouched numbers, comments, whitespace, and each existing line ending, including a missing final newline. Appends retain all existing bytes and the final-newline convention; an unterminated file uses its dominant LF/CRLF convention to separate the new record.
  • YAML leaf writes coerce to the existing scalar type (string, finite number, true/false, or null). YAML insertions use the bundled yaml package's document API for map/sequence updates. Malformed YAML documents with parser errors are refused before mutation with parse-error. Leaf replacement through object-valued mapping keys, such as YAML 1.1 timestamp keys, is refused with unresolved and leaves the file unchanged.

--value-json is refused for Markdown and YAML without changing the file. Pass a plain <value> without that flag to use their existing string/scalar write behavior.

Use --dry-run before user-visible writes when the exact bytes matter. JSONC and YAML edits patch the existing document (via jsonc-parser or the yaml document API); Markdown and JSONL edits preserve untouched source ranges. Add --diff when you want the preview as a focused before/after patch instead of the full rendered file. The patch records line-ending changes and missing final newlines, so applying it produces the same bytes as the write.

Examples

bash
# Validate a path (no filesystem access)openclaw path validate 'oc://AGENTS.md/Tools/$last/risk' # Read a leafopenclaw path resolve 'oc://gateway.jsonc/version' # Wildcard searchopenclaw path find 'oc://session.jsonl/*/event' --file ./logs/session.jsonl # Dry-run a writeopenclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run # Dry-run a write as a unified diffopenclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run --diff # Apply the writeopenclaw path set 'oc://gateway.jsonc/version' '2.0' # Byte-fidelity round-trip (diagnostic)openclaw path emit ./AGENTS.md

More grammar examples:

bash
# 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 subsegmentsopenclaw path set 'oc://openclaw.json/agents/list/0/tools/exec/security' 'allowlist' --dry-run # Replace a JSONC leaf with a parsed objectopenclaw path set 'oc://openclaw.json/gateway/auth/token' '{"source":"file","provider":"secrets","id":"/test"}' --value-json --dry-run # Predicate search over JSONC childrenopenclaw path find 'oc://config.jsonc/plugins/[enabled=true]/id' # Insert into a JSONC arrayopenclaw path set 'oc://config.jsonc/items/+1' '{"id":"new","enabled":true}' --dry-run # Insert a JSONC object keyopenclaw path set 'oc://config.jsonc/plugins/+github' '{"enabled":true}' --dry-run # Append a JSONL eventopenclaw path set 'oc://session.jsonl/+' '{"event":"checkpoint","ok":true}' --file ./logs/session.jsonl # Resolve the last JSONL value lineopenclaw path resolve 'oc://session.jsonl/$last/event' --file ./logs/session.jsonl # Resolve a YAML workflow stepopenclaw path resolve 'oc://workflow.yaml/steps/0/id' # Update a YAML scalaropenclaw path set 'oc://workflow.yaml/steps/$last/id' 'classify-renamed' --dry-run # Address markdown frontmatteropenclaw path resolve 'oc://AGENTS.md/[frontmatter]/name' # Find several markdown frontmatter keysopenclaw path find 'oc://AGENTS.md/[frontmatter]/{name,description}' # Insert markdown frontmatteropenclaw path set 'oc://AGENTS.md/[frontmatter]/+description' 'Agent instructions' --dry-run # Find markdown item fieldsopenclaw path find 'oc://SKILL.md/Tools/*/send_email' # Validate a session-scoped pathopenclaw path validate 'oc://AGENTS.md/Tools/$last/risk?session=cron-daily'

Recipes by file kind

The same five verbs work across kinds; the addressing scheme dispatches on the file extension.

Markdown

text
<!-- frontmatter.md -->---name: drafterdescription: email drafting agenttier: core---## Tools- gh: GitHub CLI- curl: HTTP client- send_email: enabled
bash
$ openclaw path resolve 'oc://x.md/[frontmatter]/tier' --file frontmatter.md --humanleaf @ L4: "core" (string) $ openclaw path resolve 'oc://x.md/tools/gh/gh' --file frontmatter.md --humanleaf @ L9: "GitHub CLI" (string) $ openclaw path find 'oc://x.md/tools/*' --file frontmatter.md --human3 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 keep their slug form even when the source uses underscores (send_email becomes send-email).

JSONC

text
// config.jsonc{  "plugins": {    "github": {"enabled": true, "role": "vcs"},    "slack":  {"enabled": false, "role": "chat"}  }}
bash
$ openclaw path resolve 'oc://config.jsonc/plugins/github/enabled' --file config.jsonc --humanleaf @ 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.

For named object keys, JSON, JSONC, and JSONL reads, searches, and replacements use the last duplicate occurrence, matching runtime parsing. Replacements preserve earlier shadowed copies. Insertion beneath a duplicated parent is refused: consolidate the duplicate parents into one, retaining the last effective value, then retry.

JSONL

text
{"event":"start","userId":"u1","ts":1}{"event":"action","userId":"u1","ts":2}{"event":"end","userId":"u1","ts":3}
bash
$ openclaw path find 'oc://session.jsonl/[event=action]/userId' --file session.jsonl --human1 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 --humanleaf @ L2: "2" (number)

Each line is a record. Address by predicate ([event=action]) when you do not know the line number, or by the standard LN segment when you do. .ndjson files use the same adapter as .jsonl.

YAML

text
# workflow.yamlname: inbox-triagesteps:  - id: fetch    command: gmail.search  - id: classify    command: openclaw.invoke
bash
$ openclaw path resolve 'oc://workflow.yaml/steps/0/id' --file workflow.yaml --humanleaf @ 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.yamlname: inbox-triagesteps:  - 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.

resolve and find follow aliases to their anchored values, including predicate fields. Reported lines refer to the anchor. set can replace an alias slot without changing its anchor, but writes through an alias remain refused. Recursive searches stop at cyclic alias edges while retaining distinct logical paths. YAML merge keys (<<) are not expanded.

Subcommand reference

resolve <oc-path>

Read a single leaf or node. Wildcards are rejected — use find for those. Exits 0 on a match, 1 on a clean miss, 2 on a parse error or refused pattern.

bash
openclaw path resolve 'oc://AGENTS.md/tools/gh/risk' --humanopenclaw path resolve 'oc://gateway.jsonc/server/port' --json

find <pattern>

Enumerate every match for a wildcard / predicate / union pattern. Exits 0 on at least one match, 1 on zero. File-slot wildcards are rejected with OC_PATH_FILE_WILDCARD_UNSUPPORTED and exit 2 — pass a concrete file path. Quote a literal ? in a filename, for example oc://"report?.jsonc"/items/*.

bash
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 leaf. Pair with --dry-run to preview the bytes that would be written without touching the file. Add --diff for a unified diff preview. Exits 0 on a successful write, 1 if the substrate refuses (for example, a sentinel guard hit), 2 on parse errors.

bash
openclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-runopenclaw path set 'oc://gateway.jsonc/version' '2.0' --dry-run --diffopenclaw 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 + work for indexed and append insertion respectively.

validate <oc-path>

Parse-only check. No filesystem access. Useful when you want to confirm a template path is well-formed before substituting variables, or when you want the structural breakdown for debugging:

bash
$ openclaw path validate 'oc://AGENTS.md/tools/gh' --humanvalid: oc://AGENTS.md/tools/gh  file:    AGENTS.md  section: tools  item:    gh

Exits 0 when valid and 1 when invalid (with a structured code and message). Missing required arguments are rejected by Commander with exit 1.

emit <file>

Round-trip a file through the per-kind parser and emitter. The output should be byte-identical to the input on a sound file; divergence indicates a parser bug or a sentinel hit. Useful for debugging substrate behavior on real-world inputs.

bash
openclaw path emit ./AGENTS.mdopenclaw path emit ./gateway.jsonc --json

Exit codes

Code Meaning
0 Success. (resolve / find: at least one match. set: write succeeded.)
1 No match, invalid validate input, missing required arguments, or set rejected by the substrate.
2 Path or file parse error, refused pattern, or invalid mutation options.

Output mode

openclaw path is TTY-aware: human-readable output on a terminal, JSON when stdout is piped or redirected. --json and --human override the auto-detection.

Path/file parse errors and refused patterns in JSON mode write one error object to stdout, with ok: false and an error containing type: "cli_error", the domain code, and message. Human-mode diagnostics remain on stderr. Refusals still return a nonzero exit code.

Notes

  • set refuses string leaf values containing __OPENCLAW_REDACTED__ (verbatim or as a substring) before a write or dry-run preview. This includes Markdown insertion values for frontmatter, items, and headings. Ordinary edits retain unrelated pre-existing marker text.
  • JSONC parsing and leaf edits use the plugin-local jsonc-parser dependency, so comments and formatting are preserved on ordinary leaf writes instead of going through a hand-rolled parser/re-render path.
  • path is not aware of last-known-good (LKG) config tracking or recovery; that lifecycle is owned elsewhere. If a file you edit through path is also LKG-tracked, the next config read decides whether to promote or recover it; treat a path edit the same as any other direct write to that file.
Was this useful?