A focused .NET library that parses bash command strings into a structured AST. Purpose-built for tools that need to reason about shell commands without running them β approval gates for LLM-emitted commands, CI/CD script auditors, sandbox policy generators, audit-log analytics.
Hand-rolled, AOT-trim friendly, zero native dependencies. Multi-targets
netstandard2.0 and net8.0.
dotnet add package ShellSyntaxTree --version 0.1.0-alphaFor an input like cd /repo && rm /etc/passwd, ShellSyntaxTree produces:
flowchart TD
classDef bad fill:#fee,stroke:#b00,stroke-width:2px
A[cd /repo<br/>π /repo] -- "&&" --> B[rm<br/>π /etc/passwd<br/>cwd: /repo]
class B bad
A two-clause AST where the second clause's Args includes a synthetic
/repo attribution arg (so consumers can see "this rm is implicitly
operating in /repo") and /etc/passwd is resolved and marked
IsPath = true. Hard-deny rules over /etc/* fire immediately; no
substring matching, no shelling out, no false positives.
- Approval gates for AI agents β given a command emitted by an LLM, decide ALLOW / PROMPT / DENY before invoking the shell.
- CI/CD pipeline audits β scan shell steps in GitHub Actions /
Jenkinsfile / Azure Pipelines for writes outside the workspace,
curl | bashfrom non-allowlisted hosts, hardcoded credential echoes. - Sandbox / container policy β derive the minimum-viable volume mount set or AppArmor profile from a build script.
- Pre-commit linters β flag dangerous patterns (
rm -rf /,chmod 777 /etc/*) in shell scripts at commit time. - Shell history / audit-log analytics β ingest
~/.bash_historyorauditdrecords into structured form for SIEM-style insights. - Documentation / explainers β convert complex one-liners into readable structure for tutorials and runbooks.
The original consumer is Netclaw's approval policy; the library is built to be reusable beyond that.
using ShellSyntaxTree;
var parser = new BashParser();
var parsed = parser.Parse("cd /repo && rm /etc/passwd");
if (parsed.IsUnparseable)
{
// Safe-fail: prompt the user, deny the command, etc.
Console.WriteLine($"can't model: {parsed.UnparseableReason}");
return;
}
foreach (var clause in parsed.Clauses)
{
Console.WriteLine($"{clause.Operator} {clause.Verb.Joined}");
foreach (var arg in clause.Args.Where(a => a.IsPath))
{
var marker = arg.IsCwdAttribution ? "β³ cwd" : " path";
Console.WriteLine($" {marker}: {arg.Resolved}");
}
foreach (var redirect in clause.Redirects.Where(r => !r.IsDynamicSkip))
{
Console.WriteLine($" {redirect.Direction}: {redirect.Target}");
}
}Run that against the example input and you get:
None cd
path: /repo
AndIf rm
β³ cwd: /repo
path: /etc/passwd
namespace ShellSyntaxTree;
public interface IShellParser { ParsedCommand Parse(string command); }
public sealed class BashParser : IShellParser { /* β¦ */ }
public sealed record BashParserOptions { /* HomeDirectory, WorkingDirectory */ }
public sealed record ParsedCommand { /* Source, Clauses, IsUnparseable, β¦ */ }
public sealed record Clause { /* Operator, Verb, Args, Redirects, β¦ */ }
public sealed record VerbChain { /* Tokens, Joined */ }
public sealed record Arg { /* Raw, Resolved, Kind, IsPath, IsCwdAttribution, IsFlag */ }
public sealed record Redirect { /* Direction, Target, IsDynamicSkip */ }
public enum ArgKind { Literal, EnvVar, Glob, Tilde, DynamicSkip }
public enum RedirectDirection { In, Out, Append, ErrOut, ErrAppend }
public enum CompoundOperator { None, AndIf, OrIf, Sequence, Pipe }PowerShell and Windows cmd parsers are deferred to later versions; the
IShellParser seam is in place so consumers don't refactor when they
ship.
Full behavioral contract: SPEC.md.
Two runnable samples live under samples/.
dotnet run --project samples/ShellSyntaxTree.Cli.Sample -- explain "cd /repo && rm /etc/passwd"
dotnet run --project samples/ShellSyntaxTree.Cli.Sample -- audit "cd /repo && rm /etc/passwd"explain pretty-prints the AST with [flag] / [path] / [cwd-attr] /
[dyn-skip] / [glob] markers per arg. audit runs a small built-in
policy ("deny writes in /etc, /usr, /bin, /sbin, /lib",
"warn on curl | bash", "warn on dynamic args in path slots") and
exits 0 / 1 / 2 by severity. See
samples/ShellSyntaxTree.Cli.Sample/Commands/AuditPolicy.cs
for the policy code β ~50 lines.
Paste a bash script, watch the parsed AST render as a Mermaid flowchart
in your browser. Everything runs client-side β pasted scripts never
leave your machine. Useful for "what does this script actually do?"
moments and for understanding how the library models constructs like
subshells and bash -c recursion.
dotnet run --project samples/ShellSyntaxTree.Web.Sample
# β http://localhost:5239The visualizer ships preset scripts demonstrating compound commands,
subshell isolation, bash -c recursion, dynamic-cwd attribution, and
unparseable inputs (control-flow, function definitions). Each preset
shows what the library produces in a single click.
dotnet tool restore
dotnet build -c Release
dotnet test -c Release
dotnet pack -c Release -o ./bin/nugetglobal.json pins the SDK; you need .NET 10 SDK or later for the
.slnx solution format.
Tags are bare SemVer version numbers β no v prefix. The release
workflow asserts this and fails fast on misformatted tags.
- 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.
- 1.0.0 β when an external consumer beyond Netclaw ships against it without finding API gaps.
Apache-2.0. Copyright Β© 2026 Aaron Stannard.
Repository layout β for contributors and curious agents:
| Path | What |
|---|---|
src/ShellSyntaxTree/ |
The library |
tests/ShellSyntaxTree.Tests/ |
xUnit unit tests + corpus runner |
tests/ShellSyntaxTree.Tests/Corpus/bash/*.json |
115 corpus entries β the acceptance contract |
samples/ShellSyntaxTree.Cli.Sample/ |
Console explainer + audit policy |
samples/ShellSyntaxTree.Web.Sample/ |
Blazor WASM Mermaid visualizer |
SPEC.md |
Locked v0.1 contract |
openspec/ |
Change-proposal history (rationale for v0.1 design decisions) |
PROJECT_CONTEXT.md, TOOLING.md, AGENTS.md |
Repo governance β for autonomous agents |
IMPLEMENTATION_PLAN.md |
NOW / NEXT / LATER work tracker |
