A public implementation guide for Runmark contributors.
Runmark is designed as a local analysis core that can be embedded into an AI-agent Hook or Guardrail.
| Agent Shell call |
→ | Input AnalyzeRequest |
→ | Runmark core Parse · expand · extract |
→ | RunmarkFacts Evidence-backed facts |
→ | Decision layer Hook · Guardrail · approval |
Explicit workspace context enters at the Input stage. The decision layer owns allow, review, deny, and sandbox behavior.
The core produces facts. The surrounding integration owns policy, approval, sandboxing, and execution.
The following constraints are part of the architecture:
- The core never executes the analyzed command;
- the core never starts a child process;
- the core never fetches remote content;
- the core does not read the host filesystem implicitly;
- the core does not read the host environment as an undocumented fallback;
- the core does not call an LLM to invent effects;
- every reported fact must have traceable evidence or an explicit aggregate source;
- unknown behavior must remain visible;
- logical path analysis must not be described as operating-system isolation;
- adapters must translate protocols, not reimplement Shell analysis;
- renderers must serialize facts, not infer new facts.
These constraints keep analysis offline, reproducible, and reviewable.
The current Go project is organized around these responsibilities:
cmd/runmark/ CLI entrypoint
internal/app/ application wiring and command handlers
internal/ir/ internal request/report types and invariants
internal/analyzer/ analysis orchestration and completeness
internal/shell/ lexer, AST, parser, and stage splitting
internal/expand/ bounded npm/pnpm/make/inline-script expansion
internal/effect/ internal effect extraction rules
internal/facts/ internal report to RunmarkFacts projection
internal/render/ JSON and text rendering
internal/adapter/ client-specific Hook adapters
internal/schemacheck/ schema and fixture checks
schema/ versioned internal and experimental schemas
testdata/ fixtures and regression cases
The intended dependency direction is shown below. The analyzer owns orchestration; facts and renderers consume the internal report; adapters translate client protocols at the edge.
| CLI cmd/runmark |
→ | App internal/app |
→ | Analyzer internal/analyzer |
→ | Core packages shell · expand · effect · ir |
| Facts projection internal/facts |
→ | Renderers JSON · text |
| Client adapter internal/adapter |
→ | App + facts protocol boundary |
The internal report model should not depend on renderers or client adapters.
The core accepts a command and optional, explicitly supplied context:
type AnalyzeRequest struct {
Command string `json:"command"`
Context *AnalysisContext `json:"context,omitempty"`
}
type AnalysisContext struct {
CWD string `json:"cwd"`
Platform string `json:"platform,omitempty"`
Shell string `json:"shell,omitempty"`
Files map[string]string `json:"files,omitempty"`
Env map[string]string `json:"env,omitempty"`
}The context may contain:
- a logical working directory;
- target platform and Shell family;
- read-only contents of files such as
package.jsonandMakefile; - explicitly supplied environment values.
The core does not open those files itself. A CLI or adapter may read a context file and pass its decoded contents to the core.
Input boundaries should be validated before analysis:
- command must be non-empty and bounded in size;
- context file keys must be relative workspace paths;
- path traversal in context keys must be rejected;
- context size must be bounded;
- missing context must produce an explicit unknown, not an implicit filesystem read.
The intended pipeline is shown as a compact horizontal flow. The grouped layout keeps the main stages visible without turning the diagram into a tall stack.
|
Input Validate request ↓ Normalize paths |
→ |
Parse Lex tokens ↓ Build AST → split stages |
→ |
Analyze Bounded expansion ↓ Effects · evidence · unknowns |
→ |
Project Completeness ↓ Validate → RunmarkFacts |
The arrows represent data flow; vertical arrows inside each phase represent the local order of operations.
The parser should preserve enough structure to reason about:
- quoted words and escapes;
- redirects;
- pipelines;
&&,||, and;;- command substitutions;
- inline Shell scripts;
- source locations for evidence.
A compound command is represented as ordered stages. Conditions such as “runs after success” are preserved in the internal model.
Runmark may expand project metadata only when the caller supplies it:
npm runandpnpm runreadpackage.jsonscripts;makereads a supplied Makefile and a limited subset of targets, dependencies, recipes, and literal variables;sh -candbash -cmay parse literal inline Shell strings;- dynamic Python or interpreter code remains opaque unless it is safely representable by the supported rules.
Expansion is bounded by recursion depth, node count, input size, and the caller's cancellation context. Cycle detection uses the active expansion path, so shared scripts are not confused with recursive cycles.
Runmark never calls npm, pnpm, make, a Shell interpreter, or a remote URL during analysis.
The internal report may contain richer information than the public projection:
ImpactReport
├── command and logical cwd
├── coverage and completeness
├── ordered stages
│ ├── command and condition
│ └── internal effects
│ ├── kind and target
│ ├── certainty and provenance
│ └── evidence
├── structured unknowns
└── derived flags
The internal model supports analysis correctness, deterministic tests, and future consumers. It is not automatically a stable public API.
The experimental projection is intentionally smaller:
{
"schema_version": "0.1-touch-experimental",
"touches": {
"read": [],
"write": [],
"delete": []
},
"boundary": {
"outside_workspace": false,
"sensitive_path": false,
"destructive": false,
"external_network": false,
"opaque_script": false
},
"scripts": [],
"unknown": false,
"unknown_reasons": [],
"evidence": []
}Projection rules:
- path touches come from internal read/write/delete effects;
- paths are deduplicated and sorted deterministically;
outside_workspaceis a logical static judgment, not a sandbox result;sensitive_pathis derived from an explicit, documented sensitive-path rule set;destructiveis a factual action flag, not a risk score;- network or remote-content effects may set
external_network; - blocking unknowns may set
opaque_scriptandunknown; - script entries and evidence are retained when available;
- the projection never reparses the command or reads files.
Runmark must preserve known facts while exposing unknown behavior.
For example:
curl https://example.com/install.sh | shThe analyzer can report:
network destination: known
interpreter: known
remote script contents: unknown
remote file effects: unknown
It must not invent a concrete installation path.
Likewise, if npm run build enters a script that depends on a runtime variable, Runmark should retain the known script entry and any proven touches, then mark the remaining behavior as opaque.
A client adapter translates a Hook event into an AnalyzeRequest:
Hook event
↓
extract Shell command and explicit context
↓
Runmark analysis
↓
RunmarkFacts
↓
short context or structured payload
The adapter should:
- handle only the client events it explicitly supports;
- return a deterministic no-op for unsupported events;
- keep stdout valid for the client protocol;
- avoid leaking full commands or sensitive context in diagnostics;
- preserve timeout and cancellation behavior;
- avoid making policy decisions in the core.
The first client adapter should be selected from a currently documented Hook protocol and kept separate from the analyzer implementation.
Tests should cover the layers independently:
- lexer and source spans;
- parser and AST shape;
- stage conditions;
- effect extraction;
- npm/pnpm/make and inline-script expansion;
- active-path cycle detection;
- unknown propagation and completeness;
- path normalization and deterministic ordering;
- report validation;
- RunmarkFacts projection;
- CLI serialization;
- adapter input, output, and no-op behavior.
Important regression cases include:
echo hi > output.txt
cat README.md
rm -rf build
rm "$OUT"/*.tmp
npm run build with and without package.json
make deploy with static and dynamic recipes
curl URL | sh
echo $(cat secret.txt)
Tests must assert structured facts, unknown codes, evidence, and stable ordering. They should not rely only on a natural-language summary.
The analyzer is end-to-end: Shell parsing, internal effect rules (path read/write/delete, network, install, privilege, plus command-specific extractors for git, the write family, sed, find, xargs, tar/zip/unzip, and package managers), unknown primitives, and bounded project-script expansion (npm/pnpm/make) all feed the ImpactReport, which is projected to the experimental RunmarkFacts.
The CLI ships analyze (facts / impact / text) and two Hook adapters — hook codex and hook claude — that emit additionalContext for a Bash PreToolUse event. Both adapters auto-inject a bounded package.json / Makefile from the event cwd (disable with RUNMARK_HOOK_CONTEXT=0). install.sh registers either hook idempotently (--with-codex, --with-claude).
What is still explicitly deferred: a stable public API / installable release, external validation against real Guardrail vendors, and a stable facts contract. The experimental projection and schema may still change.
For the rationale behind this scope, see research.md. For contribution rules, see CONTRIBUTING.md.