Yoink
⚠️ Trust warning: A Yoink plan executes arbitrary shell commands on your machine. Read every
runcommand before executing a plan. Never execute a plan merely because it exists. Plans SHOULD be supplied by the operator or stored in a trusted version-controlled project.
Default to Yoink for any noninteractive bash or shell command whose output is being collected, including one trivial command. Do not bypass Yoink because a command is short or produces only one result. Use an inline plan with npx @dk/yoink; one command still provides bounded execution, stderr capture, status metadata, and a consistent bundle. For several commands, the same plan replaces multiple shell round-trips. Deterministic shell pipelines may pass one command's output to another.
If following your instructions requires several shell commands, hoist them into one Yoink plan and use the resulting bundle instead of issuing separate shell calls.
Do not use Yoink when later retrieval commands must be chosen from earlier results, unless that dependency is a deterministic shell pipeline.
Do not use Yoink for commands that require a TTY or live interactive stdin. User-requested mutations and stateful operations MAY run through Yoink when their commands are reviewed and can be planned safely. Do not use it when Yoink or npx cannot be installed or run; tell the user and use the documented plain Bash fallback.
If Yoink cannot be installed or npx cannot run it, tell the user. A plain Bash fallback is:
printf '%s\n' '--- AGENTS.md ---'
cat -- AGENTS.md
printf '%s\n' '--- README.md ---'
cat -- README.md
printf '%s\n' '--- source files ---'
rg --files src
This fallback produces plain text instead of a multipart bundle and has no plan metadata or failure aggregation.
Inline plan
Two ways to specify an inline plan: flag-based (preferred for simple cases) or JSON heredoc (for complex plans).
Flag-based inline plan
Use command-line flags to specify commands directly. This avoids JSON escaping and is more readable in Markdown:
npx @dk/yoink --run "rg --files src" --label "Source paths" --pipe --capture \
--run "sed 's/^/Piped path: /'" --label "Piped paths"
Global flags (--max-bytes, --pipefail, --no-pipefail) must come before the first --run. Command flags (--label, --timeout, --cwd, --pipe, --capture, --no-capture) apply to the most recent --run. Labels are optional; commands without labels get default names like command-0.
Example with global flag:
npx @dk/yoink --max-bytes 100000 --run "cat AGENTS.md" --label "Instructions"
Environment variables for parameterization
Use environment variables to parameterize commands instead of placeholders or template syntax. Use single quotes for the --run argument so the variable expands in the command shell, not the calling shell. Quote the variable with "${VAR}" inside the command to prevent word splitting and globbing:
ME=dmytri npx @dk/yoink --run 'echo "hi, ${ME}"' --label "Greeting"
Use braces (${VAR}) when the variable name sits next to other characters that would otherwise become part of the name:
PREFIX=src npx @dk/yoink --run 'ls "${PREFIX}_backup"' --label "List backup"
Omit braces when the variable stands alone:
ME=dmytri npx @dk/yoink --run 'echo "${ME}"' --label "Who"
This works with multiple variables and complex commands:
REPO=yoink BRANCH=main npx @dk/yoink \
--run 'git clone "https://github.com/${ME}/${REPO}"' --label "Clone" \
--run 'cd "${REPO}" && git checkout "${BRANCH}"' --label "Checkout"
Environment variables are shell-native and work seamlessly with the flag interface. Prefer this over JSON placeholders or template syntax when parameterizing plans.
JSON heredoc inline plan
When writing a plan inside Markdown instructions or any skill, prefer a quoted heredoc. It keeps commands and trust review together and avoids a temporary plan file:
npx @dk/yoink - <<'JSON'
{
"commands": [
{
"label": "Source paths",
"run": "rg --files src",
"pipe": true,
"capture": true
},
{
"label": "Piped paths",
"run": "sed 's/^/Piped path: /'"
}
]
}
JSON
Use a supplied plan file instead when the plan is reused, large, editor-validated with $schema, or intended to persist as a repository artifact.
Quoted heredocs prevent shell expansion but do not bypass Yoink's JSON parsing. Escape backslashes inside JSON string values. For example, write \\K in JSON when the command must receive \K.
Prefer JSON-safe command patterns. Use git ls-files over raw find. Use simple globs and avoid shell-metachar predicates like \(, \); they break JSON heredocs and cost turns to debug.
When a piped command's stdout streams into the next command, set "capture": true to also include that output in the bundle.
Supplied plan file
When a retrieval-plan.json file is already present, run it as the first retrieval step:
npx @dk/yoink retrieval-plan.json
Plans MAY include "$schema": "https://unpkg.com/@dk/yoink@0/plan.schema.json" for editor validation and completion. Yoink also prints the installed schema with yoink --schema.
Writing commands
Give every requested command one commands entry with a concise label and its exact run shell command. Optional fields per command:
cwd— working directory relative to Yoink's start directory, or absolutetimeout— seconds before kill (default 1)pipe— send stdout to next command's stdincapture— include stdout in the bundle. Default:trueunlesspipeistrue. Setfalseto suppress output when only side effects matter
The default timeout is 1 second. Set timeout explicitly for commands that may take longer, commonly 5 or 10 seconds. Do not rely on the default for network, package-manager, or other variable-latency commands.
Limit output at the source when possible, for example with focused search patterns or head. Use --max-bytes <n> to limit each command's captured stdout and stderr stream independently. It is not a total bundle-size limit. Truncation is reported in result metadata.
For example:
npx @dk/yoink --max-bytes 100000 - <<'JSON'
{"commands":[{"label":"Instructions","run":"cat -- AGENTS.md"}]}
JSON
Capture is also intentional: standalone commands default to capture: true, while piped commands default to capture: false. Use capture: false for noisy output when only command metadata matters, and use capture: true on a piped command when its output must also remain in the bundle. Every command contributes metadata, stdout, and stderr MIME parts, so bundle size grows with captured output.
Piping guidance
Use pipe when a later command needs earlier output as input. The piped command's stdout is excluded from the bundle by default. Set "capture": true to keep it in the bundle (e.g., when the piped file listing also carries needed evidence).
Commands execute serially in array order. A plan-level pipe connects adjacent commands through stdin and stdout; unrelated commands are not concurrent.
Choosing command boundaries
Use a new plan command when an operation needs its own label, metadata, timeout, captured output, or failure status. Use shell operators inside run when operations form one atomic result.
- Use plan-level
"pipe": truewhen a later command consumes earlier stdout and both results need separate observability or pipefail handling. - Use shell
|for a small private transformation where only final output matters. - Use
&&when setup and the following operation must succeed as one result. - Use separate commands for independent retrievals or separate diagnostics.
- Use
;sparingly because it can hide an earlier failure behind the final command's status. - Use
||only for one logical fallback. - Avoid
&; background processes can outlive the command, race with later steps, leak resources, and produce incomplete output.
Plan-level pipes connect stdout to stdin. They do not turn output into arguments automatically. Use a consumer such as xargs when a pipeline's data must become arguments.
Use "capture": false on a standalone command to suppress its stdout when you only want its side effects (e.g. writing a file, seeding state).
Reading the bundle
After Yoink runs, read the multipart MIME bundle from standard output. Do not recreate the plan, run its commands directly, or answer before reading the bundle.
Each command has three ordered parts: metadata JSON, captured stdout bytes, and stderr bytes. The boundary lines and Content-Disposition: form-data; name="metadata", name="stdout", name="stderr" headers mark where each part begins, so read them as the structure of the bundle rather than as content. Keep Yoink's stderr separate because it contains diagnostics. Use the metadata index or label to associate the byte parts with their command, and preserve bytes that are not valid text.
Required workflow for a supplied plan
- Run exactly
npx @dk/yoink retrieval-plan.json. - Read the multipart bundle from standard output.
- Answer the request using the bundle's results.
Pass a supplied plan file as Yoink's positional argument. Do not send its bytes through standard input.
When a later agent or process needs durable context, redirect Yoink's standard output to a file. Otherwise, do not create a plan or context file.