Repository: Aaronontheweb/ShellSyntaxTree
License: Apache-2.0
Owner: Aaron Stannard (@Aaronontheweb)
ShellSyntaxTree is a focused .NET library that parses shell command strings into a structured AST for downstream policy / security gate evaluation. It is a parser, not an interpreter — it never executes, expands, or evaluates commands.
The output is a ParsedCommand containing:
- one or more
Clauserecords (split on&&,||,;,|) - per-clause verb chain (multi-token verbs like
git push,docker compose up) - per-clause args with
IsPathclassification,Resolvedabsolute path when known, and explicitDynamicSkipmarking for unresolved env vars / unexpanded globs - redirect operators (
>,>>,<,2>,2>>) - source-ordered clause elements with exact spelling, decoded values, source spans when available, and coordinates relative to parser-classified verb elements; executable-specific semantics remain consumer-owned
- Bash
cd <dir> && cmdand PowerShellSet-Location <dir>; cmdpropagation — the target is attributed to subsequent clauses - recursion into
bash -c,pwsh -Command, andpwsh -EncodedCommandso wrapped commands surface as clauses - PowerShell alias canonicalization and explicit dynamic-command identity
- Bash subshell isolation and PowerShell grouping semantics for cwd attribution
- safe-fail flag
IsUnparseablefor unsupported constructs (control flow, function definitions, process substitution, deep command-string nesting, unbalanced quotes/parens/opaque regions)
The complete contract lives in SPEC.md and
SPEC.POWERSHELL.md.
Primary consumer: Netclaw — an
open-source autonomous operations agent. Netclaw's POSIX approval gate consumes
ShellSyntaxTree's Bash parser to decompose approval units, identify candidate
verbs and directories, propagate cwd context, inspect redirects, and fail
closed when parsing is uncertain. Its PowerShell integration is the remaining
v0.2.0 downstream acceptance item. Netclaw is expected to use the consumer
guide's general executable-aware matching path for supported commands, with
strict authored-stream matching as the fallback for unrecognized shapes. See
docs/CONSUMER_GUIDE.md for the public consumer
algorithm and immutable Netclaw examples.
Acceptance is tied to Netclaw integration (see SPEC §17 #7-#8): the
package must be consumable via <PackageReference> and exercise at least
one corpus entry through Netclaw's live matcher.
Secondary consumers: any tool that needs to reason about the shape of agent-emitted shell commands without executing them — pre-commit hooks, audit pipelines, sandbox policy engines, IDE security extensions.
LLM-driven agents emit shell commands. Naive substring matching is unsafe
(rm /tmp/foo vs rm -rf $HOME/foo); shelling out to bash -n is unsafe
and doesn't yield AST nodes; tree-sitter-bash is overkill and ships native
binaries. ShellSyntaxTree fills the middle: a hand-rolled, AOT-friendly,
zero-native-deps .NET parser sized to what security gates actually need.
- Bash and PowerShell 7 pipeline parsing ship behind the shared
IShellParserseam. Windowscmdremains deferred. - Public API surface in SPEC §2 is locked. Internal changes are free.
- Acceptance is the multi-shell corpus contract: every Bash and PowerShell
JSON entry parses to its expected AST, and the PowerShell corpus also passes
the live
pwshoracle matrix.
- Command execution.
- Variable expansion of any kind (we mark dynamic tokens, never resolve
them;
$HOMEis the only exception). - Heredoc body extraction.
- Process substitution
<(cmd),>(cmd). - Function definitions,
for/while/casecontrol flow, arithmetic expansion$((...)). - PowerShell script-level control flow, definitions, expression evaluation,
and
.ps1file-content parsing. - Performance optimization beyond "fast enough to invoke per shell call without noticeable latency" (~1 ms typical).
- Full IDE-style source mapping.
Clause.Elementsprovides security-motivated provenance for significant clause leaves, not a lossless concrete syntax tree.
0.1.0-alpha— first publishable cut, Bash-only.0.1.x— additive (more verb table entries, more corpus, bug fixes).0.2.0— first PowerShell parser implementation; alpha and beta.1 shipped, stable promotion pending downstream validation.1.0.0— at least one external consumer beyond Netclaw ships against it without finding API gaps.
- Public API in
SPEC.md§2 is the contract. Everything else isinternal. During0.x, renaming or removing public fields requires a deliberate minor version bump and migration notes; after1.0, it requires a major version bump. IShellParseris the multi-shell seam. Additional parsers such as Windowscmdmust be addable without reshaping consumer code.- No native dependencies. AOT-trim friendly; ship a single managed package.
- Multi-targeting:
netstandard2.0for broad consumer reach,net8.0for modern runtimes, tests onnet10.0. - Security defaults bias: when in doubt, mark
DynamicSkip/IsUnparseable. Consumers can always relax — they cannot retroactively un-execute a command we falsely classified as safe.
Per SPEC §17, all of the following must be true:
- Public API matches SPEC §2 exactly.
dotnet packproduces aShellSyntaxTree.0.1.0-alpha.nupkg. - Every corpus entry in
tests/ShellSyntaxTree.Tests/Corpus/bash/*.jsonparses to its expected AST.dotnet testruns them all and passes. - Corpus has ≥ 105 entries spanning the SPEC §13 categories.
- PII audit scan over
tests/ShellSyntaxTree.Tests/Corpus/bash/*.jsonfinds zero hits. - PR validation runs on GitHub Actions and passes.
- Tagging
0.1.0-alphatriggerspublish_nuget.ymland the package appears on nuget.org. - Netclaw consumes the package via
<PackageReference>andIShellParserresolves at runtime in Netclaw's DI container. - At least one Netclaw integration test exercises a real corpus entry through Netclaw's matcher and produces the expected gate decision.
- Corpus PII. Real-world bash corpus entries seed from agent dogfood
logs (
~/.netclaw/logs/daemon-*.log) that contain usernames, repo paths, channel/thread IDs. Sanitization is mandatory and gated by a CI scan (SPEC §14). Shipping unsanitized PII is a release-blocker. - Verb/binding table drift. Bash verb tables and PowerShell alias,
binding, and per-verb tables are static data. Adding entries is cheap;
missing entries can misclassify. The corpora and
pwshoracle are the early-warning system. - Command-string recursion depth.
bash -c,pwsh -Command, andpwsh -EncodedCommandrecursion is capped at 5. Going deeper marks the resultIsUnparseablerather than risking hostile recursion. - Resolver assumptions.
WorkingDirectorydefaults to the daemon's cwd. If consumers pass the wrong cwd, relative-path attribution will silently disagree with what the selected shell would do at runtime. - API surface lock. The
0.xline commits to the documented AST shape; breaking changes require a deliberate minor bump and migration notes.
| Concern | Location |
|---|---|
| Library source | src/ShellSyntaxTree/ |
| Tests + corpus | tests/ShellSyntaxTree.Tests/ |
| Corpus entries | tests/ShellSyntaxTree.Tests/Corpus/{bash,powershell}/*.json |
| The contracts | SPEC.md, SPEC.POWERSHELL.md |
| Consumer guide | docs/CONSUMER_GUIDE.md |
| Active work plan | IMPLEMENTATION_PLAN.md |
| Tooling inventory | TOOLING.md |
| Agent constitution | AGENTS.md (and CLAUDE.md) |
| CI | .github/workflows/{pr_validation,publish_nuget}.yml |
| Build settings | Directory.Build.props, Directory.Packages.props, global.json |
| Solution | ShellSyntaxTree.slnx |
This file is mutable but should change only when the project's purpose,
audience, scope, or constraints actually shift. Day-to-day work tracking
belongs in IMPLEMENTATION_PLAN.md. Tooling changes belong in TOOLING.md.
The agent constitution (AGENTS.md) should rarely change.