You are working on the CLI execution layer — the bridge between assembled prompts and the claude -p / codex exec CLIs, plus skill file I/O.
Key Concepts
- Stream-JSON protocol (Claude):
runClaude()always passes--verbose --output-format stream-json. Output is NDJSON:type: 'result'has final text + usage;type: 'assistant'has text/tool_use blocks;type: 'user'has tool_result blocks. - JSONL protocol (Codex):
runCodex()spawnscodex exec --json --sandbox read-only --ephemeral. The--ask-for-approval neverflag is conditionally included based on capability detection (see below). Prompt is passed via stdin ('-'placeholder arg) to avoid shell arg length limits. Stdin write happens after event handlers are attached so fast failures are captured. Events:item.completed/item.updatedwith normalized types. - Codex capability detection:
getCodexExecCapabilities()(internal, cached) runscodex exec --helpand checks if--ask-for-approvalappears in the help text. Result is cached in module-levelcodexExecCapabilitiesvariable. If the help check fails (e.g., codex not installed), capabilities default to{ supportsAskForApproval: false }.runCodex()only adds--ask-for-approval neverwhensupportsAskForApprovalis true. - Unified routing:
runLLM(prompt, options, backendId)is the shared entry point — dispatches torunClaude()orrunCodex()based onbackendId. Exported fromrunner.jsso command handlers no longer need local routing helpers. - Codex internals (private):
normalizeCodexItemType()converts PascalCase/kebab-case to snake_case.collectCodexText()recursively extracts text from nested event content. Both are internal to runner.js. - Prompt templating:
loadPrompt(name, vars)resolves{{partial-name}}fromsrc/prompts/partials/first, then substitutes{{varName}}fromvars. Target-specific vars (skillsDir,skillFilename,instructionsFile,configDir) are passed by command handlers. - File output parsing: Primary:
<file path="...">content</file>XML tags. Fallback:<!-- file: path -->comment markers.parseFileOutput(output, allowedPaths)accepts optional{ dirPrefixes, exactFiles }to override default allowed paths. - Path sanitization:
sanitizePath(rawPath, allowedPaths)(internal) blocks..traversal, absolute paths. Defaults:.claude/prefix +CLAUDE.mdexact. Multi-target callers pass expanded allowed paths viagetAllowedPaths()fromtarget.js. - Validation:
validateSkillFiles()checks for truncation (XML tag collisions), missing frontmatter, missing sections, bad file path references. - Skill rules generation:
extractRulesFromSkills()reads all skills viaskill-reader.js, producesskill-rules.json(v2.0) with file patterns, keywords, and intent patterns. - Domain patterns:
generateDomainPatterns()converts file patterns to bashdetect_skill_domain()function usingBEGIN/ENDmarkers. - Trigger parsing precedence:
parseTriggersFrontmatter(content)returns{ filePatterns, keywords, alwaysActivate }parsed from atriggers:block in YAML frontmatter (supports block lists, inline arrays, andalwaysActivate: truefor the base skill); returnsnullwhen notriggers:key exists.parseActivationPatternsandparseKeywordsprefer this frontmatter when present and fall back to legacy## Activation/Keywords:line parsing for older skills. - Settings merge:
mergeSettings()merges aspens hook config into existingsettings.json. Detects aspens-managed hooks byASPENS_HOOK_MARKERS(skill-activation-prompt,graph-context-prompt,post-tool-use-tracker,save-tokens-statusline,save-tokens-prompt-guard,save-tokens-precompact). Also handlesstatusLinemerging — replaces existing statusLine only if the current one is aspens-managed (detected byisAspensHook), preserving user-custom statusLine configs. After merging hooks,dedupeAspensHookEntries()removes duplicate aspens-managed entries per event type. - Directory-scoped writes:
writeTransformedFiles()handles files outside.claude/(e.g.,src/billing/AGENTS.md) with explicit path allowlist — onlyCLAUDE.md,AGENTS.mdexact files and.claude/,.agents/,.codex/prefixes are permitted. findSkillFilesmatching: Only matches the exactskillFilename(e.g.,skill.mdorSKILL.md), not arbitrary.mdfiles in the skills directory.
Critical Rules
- Both
--verboseand--output-format stream-jsonare required for Claude — omitting either breaks stream parsing. - Codex uses
--json --sandbox read-only --ephemeral—--sandbox read-onlyrestricts filesystem access,--ephemeralavoids persisting conversation.--ask-for-approval neveris added only ifgetCodexExecCapabilities()confirms support. Prompt goes via stdin, not as a CLI arg. - Codex stdin write order matters — event handlers (
stdout,stderr,close,error) must be attached before writing to stdin, so fast failures are captured. - Path sanitization is non-negotiable —
sanitizePath()blocks..traversal, absolute paths, and any path not in the allowed set. - Prompt partials resolve before variables —
{{skill-format}}resolves topartials/skill-format.mdfirst. If no file, falls through to variable substitution. - Timeout resolution:
resolveTimeout(flagValue, fallbackSeconds)—--timeoutflag wins, thenASPENS_TIMEOUTenv, then caller-provided fallback. Size-based defaults (small: 120s, medium: 300s, large: 600s, very-large: 900s) are set by command handlers, not runner. - Disk writes are sanitized —
writeSkillFilesandwriteTransformedFilespass every payload throughsanitizePublishedContentso forbidden blocks (## Activation,## Key Files, hub/cluster/hotspot tables outsidecode-map.md) cannot leak to disk even if an earlier stage missed them. mergeSettingspreserves non-aspens hooks and statusLine — identifies aspens hooks byASPENS_HOOK_MARKERS(now includes save-tokens markers), replaces matching entries, preserves everything else. StatusLine only replaced if current one is aspens-managed. Post-merge deduplication ensures no duplicate aspens entries accumulate.- Debug mode: Set
ASPENS_DEBUG=1to dump raw stream-json to$TMPDIR/aspens-debug-stream.json(Claude) or$TMPDIR/aspens-debug-codex-stream.json(Codex). Codex also logs exit code and output length to stderr.
Last Updated: 2026-05-11