Jo
Claude Code agent skills. Plain markdown. Highly opinionated.
npx -y skills add petr-korobeinikov/skills --skill joAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Prefers `jo` (https://github.com/jpmens/jo) for constructing JSON in shell snippets over hand-written quoted JSON literals. Removes quote-escaping pitfalls, makes variable interpolation natural, and lets `jo` infer field types from values (with explicit coercion when needed).
SKILL.md
8.5 KB, as published. Nobody here has run it
jo
Build JSON in shell snippets with jo,
not by hand-typing a quoted JSON literal.
jo handles JSON quoting internally,
so shell variables interpolate cleanly
and you never juggle nested single/double quotes.
Authoritative documentation:
https://github.com/jpmens/jo
(README + jo.md man page).
Verify any non-trivial form against the manual before relying on it.
Why
- Hand-written JSON in a shell string is a quote-escaping minefield.
Inserting
$VARinto'{"user":"x"}'either breaks the single quotes or forces an awkward'"$VAR"'pattern that is hard to read and easy to mistype. - Typos in inline JSON (missing comma, stray quote, trailing comma) fail at the server, not at parse time — hard to spot in a long single-line payload.
jofigures out types from the value: numbers, booleans,null, nested objects, and arrays are detected automatically; override with-s/-n/-bper word when the guess would be wrong (e.g. a numeric ID that must stay a string).jocomposes: shell variables, command substitution, bracket-path shorthands, and nestedjocalls all work without re-escaping.
How jo types a value
Per the man page, the value pipeline is:
- Try to parse the value as JSON.
5→ number,true/false→ boolean,null→ null,{...}/[...]→ embedded object/array, strings that look numeric but aren't valid floats (e.g.3.14159.26) → string. - If JSON parse fails,
check special value prefixes:
@file,%file,:file(see Reading from files below). - Otherwise it's a literal string.
A missing/empty value (k=) becomes null.
Pass -B to disable detection of the literal strings true/false/null.
Globally,
-n drops keys with empty values
(not to be confused with the per-word -n coercion after --).
Direct substitutions
| Don't | Do |
|---|---|
'{"name":"alice"}' | "$(jo name=alice)" |
'{"name":"'"$USER"'"}' | "$(jo name="$USER")" |
'{"count":5,"active":true}' | "$(jo count=5 active=true)" |
'{"value":null}' | "$(jo value=null)" (or "$(jo value=)") |
'{"tags":["a","b","c"]}' | "$(jo tags="$(jo -a a b c)")" |
'{"point":[1,2]}' | "$(jo 'point[]=1' 'point[]=2')" |
'{"geo":{"lat":10,"lon":20}}' | "$(jo 'geo[lat]=10' 'geo[lon]=20')" |
'{"user":{"id":1,"name":"alice"}}' | "$(jo user="$(jo id=1 name=alice)")" |
'{"code":"12345"}' (string ID) | "$(jo -- -s code=12345)" |
Bracket-path forms (key[]=, key[sub]=) are one level deep only —
'a[b][c]=1' silently drops [c] and produces {"a":{"b":1}}.
For deeper nesting,
use nested jo via command substitution.
Syntax cheatsheet
Value forms
k=v— JSON-parsed then prefix-checked then literal string (see Howjotypes a value).k@v— boolean from a truthy value (T/tor non-zero numeric →true, elsefalse); handy forflag@$ENABLED.k=@file— loadfilecontents as a string value.k=%file— loadfileas base64-encoded string.k=:file— parsefileas JSON and embed.k:=fileork:=-— same ask=:filebut as an operator;-reads JSON from stdin.
To use a literal @, %, or : at the start of a string value,
escape it with \:
twitter='\@jpmens',
vimcmd='\:split',
uri='\\%20'.
Bracket paths (one level)
'name[]=v1' 'name[]=v2'— appends to thenamearray.'obj[k]=v'— creates{"obj":{"k":v}}.- Combine
-d.(path separator) for dot notation:jo -d. geo.lat=10 geo.lon=20is equivalent tojo 'geo[lat]=10' 'geo[lon]=20'. - Always quote bracket forms —
unquoted
[/]get caught by zsh globbing (and bash too, if any expansion matches).
Arrays
jo -a v1 v2 v3— array of auto-typed values.jo -a -- -s 123 hello -n "five"— per-word coercion inside an array (here:["123","hello",4]—-non a non-numeric string yields its length).
Type coercion
jo -- -s k=v— force string. Use for numeric IDs, version strings, phone numbers, ZIPs.jo -- -n k=v— force number (non-numeric strings become their length).jo -- -b k=v— force boolean (empty →false, anything else →true).--is mandatory before the word list whenever the first word starts with-; pass it always for consistency with the manual.
Global options
The two most-used:
-p— pretty-print (drop for production payloads).-a— emit an array instead of an object.
For the full option list
(-B, -D, -n global, -d sep, -f file, -e, -o, -v/-V,
plus per-word coercion semantics after --)
see references/options.md.
Idioms
curl POST with a body:
curl -X POST https://api.example.com/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jo name="$NAME" email="$EMAIL" active=true)"
Bracket shorthand for one-level nesting
(no nested jo substitution needed):
curl -X POST https://api.example.com/v1/orders \
-H "Authorization: Bearer $TOKEN" \
-d "$(jo \
'customer[id]='"$CUSTOMER_ID" \
'customer[name]='"$CUSTOMER_NAME" \
'items[]='"$SKU_A" \
'items[]='"$SKU_B")"
Nested deeper than one level — build via command substitution (or stash in a variable):
payload=$(jo \
name="$NAME" \
address="$(jo street="$STREET" city="$CITY")" \
tags="$(jo -a admin user)")
curl -X POST -d "$payload" https://api.example.com/v1/users
kubectl patch:
kubectl patch deployment web \
--type=merge \
--patch "$(jo spec="$(jo replicas=3)")"
Numeric ID that must stay a string
(otherwise jo emits "id":12345 as a number):
curl -X POST -d "$(jo -- -s id=12345 name="$NAME")" \
https://api.example.com/v1/users
Modify an existing JSON document:
curl -sSL https://api.example.com/v1/config \
| jo -f - updated_at="$(date -Iseconds)" version="$NEW_VERSION"
When NOT to use jo
Large, fixed JSON document
A heredoc with literal JSON is clearer than a long chain of jo calls,
and the JSON can be copy-pasted from API docs without translation:
curl -X POST -d @- https://api.example.com/v1/config <<'JSON'
{
"feature_flags": {
"alpha": true,
"beta": false
},
"limits": {
"rps": 100,
"burst": 200
}
}
JSON
jo not available on the host
Some minimal CI images or locked-down servers lack it. Fall back to a heredoc; do not fall back to fragile inline quoted JSON.
Installing jo
- macOS:
brew install joorsudo port install jo - Debian/Ubuntu:
apt install jo - Fedora:
dnf install jo - Alpine:
apk add jo - Gentoo:
emerge jo - Arch:
pacman -S jo(inextra) - Snap:
snap install jo - Windows:
scoop install jo - Docker:
docker run --rm jpmens/jo ... - From source: https://github.com/jpmens/jo
jo is not in the default aqua/mise short-name registry;
on a mise-managed host install via the package manager
or via ubi:jpmens/jo.
Scope
Applies anywhere a shell snippet emits JSON:
Bashtool invocations.- Snippets in
SKILL.md/README.md/ docs. - Scripts under
scripts/. - Makefile recipes.
- CI configs.
- API smoke-test instructions.
If you encounter a hand-written inline JSON string while editing a file,
replace it with the equivalent jo form as part of the same edit.