diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index a91fef6..df651ce 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -54,6 +54,11 @@ priorities. Stop the Bash and PowerShell native greedy passes before a token that matches the shared path-shape rules. Preserve that token as a resolved argument without a command dictionary or a public API change. +- [x] **Consumer guide.** Added `docs/CONSUMER_GUIDE.md` with the + shell-neutral security-consumer algorithm, Bash and PowerShell guidance, + worked public use cases, and immutable permalinks to Netclaw's production + integration. Linked it from the README and aligned stale PowerShell + prerelease/status wording in the public project docs. - [x] **Issue #52 — hyphenated PowerShell parameters/native options.** Preserve internal hyphens, apply bash-compatible native `--flag=value` splitting and path classification, keep colon binding @@ -85,8 +90,6 @@ priorities. - Seed corpus entries from sanitized real-world dogfood logs (SPEC §14 workflow) — both shells. - Expand verb / cmdlet / alias tables as the corpus surfaces real commands. -- Document the "consumer's algorithm" — given a `ParsedCommand`, how a - security gate walks it (a `docs/CONSUMER_GUIDE.md` or `SPEC.md` appendix). - Performance sanity check (~1 ms typical) with a tiny BenchmarkDotNet harness — only if anything in the daemon hot path complains. diff --git a/PROJECT_CONTEXT.md b/PROJECT_CONTEXT.md index 0c05623..f435404 100644 --- a/PROJECT_CONTEXT.md +++ b/PROJECT_CONTEXT.md @@ -19,24 +19,29 @@ The output is a `ParsedCommand` containing: when known, and explicit `DynamicSkip` marking for unresolved env vars / unexpanded globs - redirect operators (`>`, `>>`, `<`, `2>`, `2>>`) -- `cd && cmd` propagation — the cd target is attributed to subsequent - clauses in the same compound -- recursion into `bash -c ""` so wrapped commands surface as clauses -- subshell `( ... )` isolation for `cd` attribution +- Bash `cd && cmd` and PowerShell `Set-Location ; cmd` + propagation — the target is attributed to subsequent clauses +- recursion into `bash -c`, `pwsh -Command`, and `pwsh -EncodedCommand` so + 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 `IsUnparseable` for unsupported constructs (control flow, - function definitions, process substitution, deep `bash -c` nesting, - unbalanced quotes/parens) + function definitions, process substitution, deep command-string nesting, + unbalanced quotes/parens/opaque regions) -The complete contract lives in [`SPEC.md`](./SPEC.md). +The complete contract lives in [`SPEC.md`](./SPEC.md) and +[`SPEC.POWERSHELL.md`](./SPEC.POWERSHELL.md). ## Who It's For **Primary consumer: [Netclaw](https://github.com/netclaw-dev/netclaw)** — an -open-source autonomous operations agent. Netclaw's approval gate currently -hand-rolls equivalent functionality in -`src/Netclaw.Security/ShellApprovalSemantics.cs` and `ShellTokenizer.cs`. -ShellSyntaxTree v0.1 is intended to **replace that hand-rolled approximation** -with a richer, structured AST that Netclaw's gate can walk directly. +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. See +[`docs/CONSUMER_GUIDE.md`](./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 `` and exercise at least @@ -56,15 +61,16 @@ zero-native-deps .NET parser sized to what security gates actually need. ## Scope Discipline -### v0.1 (current) +### v0.2 (current prerelease line) -- Bash only. PowerShell and Windows `cmd` are deferred — but the - `IShellParser` seam is in place so consumers don't have to refactor. +- Bash and PowerShell 7 pipeline parsing ship behind the shared + `IShellParser` seam. Windows `cmd` remains deferred. - Public API surface in SPEC §2 is **locked**. Internal changes are free. -- Acceptance is the corpus contract (SPEC §13): every JSON entry parses to - its expected AST. +- 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 `pwsh` oracle matrix. -### Explicit non-goals (v0.1) +### Explicit non-goals - Command execution. - Variable expansion of any kind (we **mark** dynamic tokens, never resolve @@ -73,25 +79,30 @@ zero-native-deps .NET parser sized to what security gates actually need. - Process substitution `<(cmd)`, `>(cmd)`. - Function definitions, `for`/`while`/`case` control flow, arithmetic expansion `$((...))`. +- PowerShell script-level control flow, definitions, expression evaluation, + and `.ps1` file-content parsing. - Performance optimization beyond "fast enough to invoke per shell call without noticeable latency" (~1 ms typical). -- Source-mapping (line/column for AST nodes — useful for IDEs, irrelevant - for security gates). +- Full IDE-style source mapping. Security-motivated token provenance is under + active design in issue #62. ### Versioning -- `v0.1.0-alpha` — first publishable cut, bash-only. -- `v0.1.x` — additive (more verb table entries, more corpus, bug fixes). -- `v0.2.0` — first PowerShell parser implementation. -- `v1.0.0` — at least one external consumer beyond Netclaw ships against it +- `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. ## Architectural Constraints - **Public API in `SPEC.md` §2 is the contract.** Everything else is - `internal`. Renaming/removing public fields requires a major version bump. -- **`IShellParser` is the multi-shell seam.** PowerShell and `cmd` parsers - must be addable without touching consumer code. + `internal`. During `0.x`, renaming or removing public fields requires a + deliberate minor version bump and migration notes; after `1.0`, it requires + a major version bump. +- **`IShellParser` is the multi-shell seam.** Additional parsers such as + Windows `cmd` must be addable without reshaping consumer code. - **No native dependencies.** AOT-trim friendly; ship a single managed package. - **Multi-targeting**: `netstandard2.0` for broad consumer reach, `net8.0` @@ -100,18 +111,20 @@ zero-native-deps .NET parser sized to what security gates actually need. `IsUnparseable`. Consumers can always relax — they cannot retroactively un-execute a command we falsely classified as safe. -## Acceptance for v0.1.0-alpha +## Shipped acceptance for 0.1.0-alpha Per SPEC §17, all of the following must be true: 1. Public API matches SPEC §2 exactly. `dotnet pack` produces a `ShellSyntaxTree.0.1.0-alpha.nupkg`. -2. Every corpus entry in `tests/Corpus/bash/*.json` parses to its expected +2. Every corpus entry in + `tests/ShellSyntaxTree.Tests/Corpus/bash/*.json` parses to its expected AST. `dotnet test` runs them all and passes. 3. Corpus has ≥ 105 entries spanning the SPEC §13 categories. -4. PII audit scan over `tests/Corpus/bash/*.json` finds zero hits. +4. PII audit scan over `tests/ShellSyntaxTree.Tests/Corpus/bash/*.json` finds + zero hits. 5. PR validation runs on GitHub Actions and passes. -6. Tagging `v0.1.0-alpha` triggers `publish_nuget.yml` and the package +6. Tagging `0.1.0-alpha` triggers `publish_nuget.yml` and the package appears on nuget.org. 7. Netclaw consumes the package via `` and `IShellParser` resolves at runtime in Netclaw's DI container. @@ -124,27 +137,28 @@ Per SPEC §17, all of the following must be true: 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 table drift**. `BashArity`, `FileVerbs`, `CwdVerbs`, and - `FlagsWithValue` are static data. Adding entries is cheap; *missing* - entries means the parser silently mis-classifies. The corpus is the +- **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 `pwsh` oracle are the early-warning system. -- **`bash -c` recursion depth**. Capped at 5 (SPEC §10). Going deeper - marks the deepest clause `IsUnparseable` so we don't blow the stack on - hostile inputs. +- **Command-string recursion depth**. `bash -c`, `pwsh -Command`, and + `pwsh -EncodedCommand` recursion is capped at 5. Going deeper marks the + result `IsUnparseable` rather than risking hostile recursion. - **Resolver assumptions**. `WorkingDirectory` defaults to the daemon's cwd. If consumers pass the wrong cwd, relative-path attribution will - silently disagree with what bash would do at runtime. Document loudly. -- **API surface lock**. v0.1 commits to the AST shape. Mistakes here cost - a major-version bump to fix. + silently disagree with what the selected shell would do at runtime. +- **API surface lock**. The `0.x` line commits to the documented AST shape; + breaking changes require a deliberate minor bump and migration notes. ## Where Things Live | Concern | Location | |---|---| -| Library source | `src/ShellSyntaxTree/` *(to be created)* | -| Tests + corpus | `tests/ShellSyntaxTree.Tests/` *(to be created)* | -| Corpus entries | `tests/ShellSyntaxTree.Tests/Corpus/bash/*.json` | -| The contract | `SPEC.md` | +| 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`) | diff --git a/README.md b/README.md index 373d1e7..5f540f2 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,12 @@ Hand-rolled, AOT-trim friendly, zero native dependencies. Multi-targets `netstandard2.0` and `net8.0`. ```bash -dotnet add package ShellSyntaxTree --version 0.2.0-alpha +dotnet add package ShellSyntaxTree --version 0.2.0-beta.1 ``` +The `0.2.0-beta.1` prerelease is required for PowerShell support. The latest +stable `0.1.x` package supports Bash only. + ## What you get For an input like `cd /repo && rm /etc/passwd`, ShellSyntaxTree produces: @@ -95,6 +98,14 @@ AndIf rm path: /etc/passwd ``` +## Consumer guide + +The [consumer guide](./docs/CONSUMER_GUIDE.md) develops the quick start into +a production-oriented algorithm: parser selection, safe-fail handling, command +identity, paths and cwd attribution, redirects, compounds and pipelines, and +PowerShell-specific alias and dynamic-command behavior. It also links to +immutable examples from Netclaw's live approval-gate integration. + ## Public API surface ```csharp @@ -146,7 +157,7 @@ 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`](./samples/ShellSyntaxTree.Cli.Sample/Commands/AuditPolicy.cs) -for the policy code — ~50 lines. +for the illustrative policy code. ### `ShellSyntaxTree.Web.Sample` — Blazor WebAssembly Mermaid visualizer @@ -163,11 +174,11 @@ dotnet run --project samples/ShellSyntaxTree.Web.Sample ![Build script preset](./assets/sample-web-build-script.png) -A shell selector switches between the bash and PowerShell parsers; each -ships preset scripts demonstrating compound commands, subshell isolation, -command-string recursion, alias resolution, dynamic-cwd attribution, and -unparseable inputs. Each preset shows what the library produces in a -single click. +A shell selector switches between the bash and PowerShell parsers. The presets +demonstrate compound commands, Bash subshell isolation, PowerShell grouping and +alias resolution, command-string recursion, dynamic-cwd attribution, and +unparseable inputs. Each preset shows what the library produces in a single +click. ## Building from source @@ -210,6 +221,7 @@ workflow asserts this and fails fast on misformatted tags. | `tests/ShellSyntaxTree.Tests/Corpus//*.json` | Corpus entries — the acceptance contract (bash + powershell) | | `samples/ShellSyntaxTree.Cli.Sample/` | Console explainer + audit policy | | `samples/ShellSyntaxTree.Web.Sample/` | Blazor WASM Mermaid visualizer | +| `docs/CONSUMER_GUIDE.md` | Production-oriented consumer algorithm + Netclaw case study | | `tools/PwshCorpusTool/` | PowerShell corpus authoring aid | | `SPEC.md`, `SPEC.POWERSHELL.md` | The behavioral contract | | `openspec/` | Change-proposal history (rationale for design decisions) | diff --git a/SPEC.POWERSHELL.md b/SPEC.POWERSHELL.md index f2f4737..d589e0c 100644 --- a/SPEC.POWERSHELL.md +++ b/SPEC.POWERSHELL.md @@ -1,8 +1,9 @@ # ShellSyntaxTree — PowerShell Specification (v0.2.0) -**Status:** Draft for v0.2.0. Approved decisions; implementation pending. -**Audience:** Whoever (human or agent) implements the ShellSyntaxTree -PowerShell parser. +**Status:** Shipped in the v0.2.0 prerelease line; stable promotion pending +downstream Netclaw validation. +**Audience:** Whoever (human or agent) implements, consumes, or maintains the +ShellSyntaxTree PowerShell parser. **Read `SPEC.md` (the bash and shared-contract specification) end-to-end first — this document specifies only what differs for PowerShell.** diff --git a/docs/CONSUMER_GUIDE.md b/docs/CONSUMER_GUIDE.md new file mode 100644 index 0000000..83ea585 --- /dev/null +++ b/docs/CONSUMER_GUIDE.md @@ -0,0 +1,430 @@ +# Consuming ShellSyntaxTree + +ShellSyntaxTree turns a shell command into facts that another system can use +without executing the command. It is designed for approval gates, CI/CD +auditors, sandbox planners, audit-log processors, and other tools that need to +reason about commands before or after execution. + +The library is deliberately not a policy engine. It reports clauses, candidate +verb chains, arguments, paths, redirects, inherited working directories, and +uncertainty. A consumer decides what those facts mean for its own domain. + +## The boundary between parsing and policy + +ShellSyntaxTree owns shell syntax: + +- splitting compounds and pipelines into clauses; +- recognizing Bash commands, PowerShell cmdlets, aliases, and native commands; +- resolving path-shaped arguments against the caller-supplied working directory; +- propagating `cd` / `Set-Location` working-directory context; +- surfacing redirects and command-string wrappers; +- marking dynamic or unsupported input so a security consumer can fail safely. + +The consumer owns policy: + +- which verbs or cmdlets are allowed; +- how broad an approval pattern should be; +- which filesystem zones are trusted; +- whether pipelines are displayed as one approval unit or several; +- executable-specific meaning, such as the difference between a Git global + option and a `git commit` option; +- the final `ALLOW`, `PROMPT`, or `DENY` decision. + +This separation is important. ShellSyntaxTree cannot safely embed the grammar +of every executable. It should preserve the source facts a command-aware +consumer needs, while remaining conservative when those facts are incomplete. + +```mermaid +flowchart TD + A["Command text + selected shell + working directory"] --> B + B{"Caller selects the matching parser"} + + subgraph SST["ShellSyntaxTree"] + C["BashParser"] + D["PwshParser"] + C --> E["Parse syntax, classify tokens, and resolve static context"] + D --> E + E --> F["ParsedCommand: ordered clauses, verbs, args, redirects, cwd, and uncertainty"] + end + + subgraph APP["Consumer-owned policy"] + G{"IsUnparseable or policy-relevant input dynamic?"} + G -->|Yes| H["Safe-fail: prompt or deny"] + G -->|No| I["Walk every clause in source order"] + I --> J["Choose command identity and evaluate paths, cwd, and redirects"] + J --> K{"ALLOW / PROMPT / DENY"} + end + + B -->|Bash| C + B -->|PowerShell| D + F --> G +``` + +The diagram is a responsibility flow, not an execution flow: parsing never +runs the command, and every decision after `ParsedCommand` belongs to the +consumer. + +## A production-shaped consumer loop + +The caller should know which shell will execute the command and select that +parser explicitly. Supplying the real working directory is equally important: +relative paths are resolved against it. + +```csharp +using ShellSyntaxTree; + +static IShellParser CreateParser(string shell, string workingDirectory) => + shell switch + { + "bash" => new BashParser(new BashParserOptions + { + WorkingDirectory = workingDirectory, + }), + "pwsh" => new PwshParser(new PwshParserOptions + { + WorkingDirectory = workingDirectory, + }), + _ => throw new ArgumentOutOfRangeException(nameof(shell)), + }; +``` + +Do not guess the shell from the command text. `rm`, `cd`, quoting, redirects, +and grouping can mean different things in Bash and PowerShell. + +Once parsed, a security-oriented consumer normally follows this sequence: + +1. Reject or prompt on an unparseable result. +2. Walk every clause; do not authorize only the first stage of a compound or + pipeline. +3. Determine a conservative command identity. +4. Evaluate explicit path arguments, inherited cwd attribution, and redirect + targets. +5. Elevate dynamic or unresolved content when it affects the policy decision. +6. Apply product-specific rules and produce a decision. + +The decision types and `Evaluate*` helpers below are application-owned +placeholders. ShellSyntaxTree supplies the parsed facts, not those policy APIs. + +```csharp +var parser = CreateParser(shell, workingDirectory); +var parsed = parser.Parse(command); + +if (parsed.IsUnparseable) +{ + // Partial clauses are diagnostic evidence, not authorization evidence. + return GateDecision.Prompt(parsed.UnparseableReason ?? "unsupported command shape"); +} + +foreach (var clause in parsed.Clauses) +{ + if (clause.Verb.IsDynamic) + { + return GateDecision.Prompt("command identity is dynamic"); + } + + var gateKey = GetGateKey(clause.Verb); + if (gateKey is null) + { + return GateDecision.Prompt("clause has no statically known command"); + } + + var decision = EvaluateClause(gateKey, clause); + if (decision.Outcome != GateOutcome.Allow) + { + return decision; + } +} + +return GateDecision.Allow(); +``` + +The example returns on the first non-allow result for brevity. A real UI may +collect every clause decision so the operator can see the complete command. + +## Choosing a command identity + +For PowerShell aliases, prefer the canonical cmdlet identity while retaining +the token the user typed for display: + +```csharp +static string? GetGateKey(VerbChain verb) +{ + if (verb.IsDynamic || verb.Tokens.Count == 0) + { + return null; + } + + return verb.CanonicalVerb ?? verb.Tokens[0]; +} +``` + +For example, parsing `gci C:\logs` preserves `gci` in `Tokens` and reports +`Get-ChildItem` in `CanonicalVerb`. A policy can gate on `Get-ChildItem`; an +audit UI can still show `gci`. + +`VerbChain` is a best-effort syntactic hint, not a complete executable grammar. +The greedy native-command walk can include bare lowercase values because a +generic parser cannot know whether `origin` is a Git remote or a subcommand. +Unknown commands should therefore default to the full extracted chain, which +produces narrower approvals and recoverable re-prompts. A consumer may shorten +the chain only when it owns command-specific knowledge that justifies doing so. + +## Evaluating arguments and paths + +An `Arg` carries several independent facts: + +- `Raw` is the user-facing token; +- `IsFlag` identifies option-shaped tokens; +- `Kind` describes literal, environment-variable, glob, tilde, or dynamic + content; +- `IsPath` says the parser classified the argument position as a path; +- `Resolved` carries a normalized path when static resolution was possible; +- `IsCwdAttribution` marks derived working-directory context rather than a + token written in that clause. + +These facts should not be collapsed into one boolean decision. A typical zone +policy might handle them as follows: + +```csharp +foreach (var arg in clause.Args) +{ + if (arg.IsCwdAttribution) + { + EvaluateInheritedDirectory(arg.Resolved, arg.Kind); + continue; + } + + if (arg.Kind == ArgKind.DynamicSkip) + { + EvaluateUnknownArgument(arg.Raw); + continue; + } + + if (!arg.IsPath) + { + continue; + } + + if (arg.Kind == ArgKind.Glob) + { + EvaluateGlobCoveringDirectory(arg.Raw); + continue; + } + + EvaluatePath(arg.Resolved ?? arg.Raw); +} +``` + +The policy decides whether an unknown argument matters. `echo $message` may be +acceptable to one product, while `Remove-Item $target` should normally prompt. +Never treat `DynamicSkip.Raw` as a statically resolved path. + +### Working-directory attribution + +For `cd /repo && cat file.txt`, the `cat` clause receives a synthetic +`IsCwdAttribution` argument for `/repo`, and `file.txt` resolves against that +directory. PowerShell provides the same contract for `Set-Location` and its +aliases. + +The attributed argument is derived context: + +- use it when evaluating where a clause operates; +- do not render it as text the user wrote in that clause; +- treat a dynamic cwd attribution as unknown context and prompt rather than + falling back to the process cwd. + +Bash subshells isolate cwd changes. PowerShell parenthesized pipelines do not: +`(Set-Location C:\repo); Get-ChildItem` changes runspace location, so the later +clause inherits that attribution. + +## Evaluating redirects + +Redirect targets are operands too. A command that appears path-free can still +write outside an allowed zone: + +```text +echo safe > /etc/profile.d/example.sh +``` + +Walk `Clause.Redirects` independently of `Args`: + +```csharp +foreach (var redirect in clause.Redirects) +{ + if (redirect.IsDynamicSkip) + { + EvaluateUnknownRedirect(redirect.Target); + continue; + } + + EvaluatePath(redirect.Target); +} +``` + +PowerShell streams 3-6 and `*>` currently map lossily onto the shared redirect +enum. The target remains available for path policy, but consumers must not use +`RedirectDirection` to recover the exact original PowerShell stream. + +## Compounds, pipelines, and wrapped commands + +`ParsedCommand.Clauses` is ordered. Each clause carries the operator that +preceded it: + +- `AndIf`, `OrIf`, and `Sequence` normally introduce a new statement; +- `Pipe` connects pipeline stages; +- `None` marks the first clause. + +A UI may group a pipeline as one approval prompt, but authorization should +still inspect every stage. `download | sh` is unsafe even if `download` alone +is allowed. + +ShellSyntaxTree also looks through supported command-string wrappers. Clauses +surfaced from `bash -c`, `pwsh -Command`, and `pwsh -EncodedCommand` carry +`IsCommandStringWrapped = true`. The outer wrapper is not the action a +verb-based policy should authorize; the surfaced inner clauses are. + +PowerShell script blocks, subexpressions, splats, and `--%` regions are opaque +and surface as `DynamicSkip`. A dynamically invoked command such as `& $exe` +sets `VerbChain.IsDynamic = true`; no verb-pattern grant should match it. + +## Safe-fail rules + +For a security gate, these conditions should prevent a durable automatic +grant: + +- `ParsedCommand.IsUnparseable` is true; +- the non-empty input produces no clauses; +- a clause has `Verb.IsDynamic` or no statically known command identity; +- a dynamic argument or redirect affects a policy-sensitive position; +- a future package version introduces an enum or AST shape the consumer has + not mapped. + +The recoverable outcome is normally a user prompt with a one-time option, or a +deny. A false-negative approval match causes another prompt; a false-positive +match can silently execute something the operator did not authorize. + +## Worked use cases + +### AI-agent approval gate + +Input: + +```text +cd /repo && rm -rf build +``` + +The consumer can derive: + +- two clauses joined by `AndIf`; +- action `rm` with flags `-rf`; +- explicit path `build`, resolved beneath `/repo`; +- inherited cwd `/repo` on the `rm` clause. + +The product can allow deletion under a disposable build directory, prompt for +an unfamiliar workspace, and deny protected system zones. + +### CI/CD auditor + +Input: + +```text +dotnet test > /tmp/test.log && curl https://example.invalid/install | bash +``` + +The auditor can inspect the redirect path, split the second statement from the +first, recognize the pipeline, and warn that downloaded content is piped into a +shell. + +### Sandbox planner + +Input: + +```text +Set-Location C:\src; Copy-Item .\out\app.dll C:\deploy\app.dll +``` + +The consumer can collect the attributed cwd and both path operands to propose +read/write mounts. It should still apply its own cmdlet policy and access-mode +rules; ShellSyntaxTree reports paths, not filesystem permissions. + +### Command-aware policy + +Input: + +```text +git -C /repo commit +git commit -C HEAD~1 +``` + +These commands demonstrate why source provenance matters. Git assigns different +meaning to `-C` based on whether it appears before or after `commit`. +[Issue #62](https://github.com/Aaronontheweb/ShellSyntaxTree/issues/62) +tracks an ordered clause-element API so a Git-aware consumer can apply that +rule without re-tokenizing `ParsedCommand.Source`. Until that API ships, the +current `Verb` and `Args` projections do not preserve their interleaving. + +When ordered elements are added, this guide should be updated in the same +change with a complete command-aware-policy example. The existing projections +should remain documented as compatibility conveniences, while the ordered view +becomes the source-provenance path for consumers that need positional meaning. + +## Netclaw case study + +[Netclaw](https://github.com/netclaw-dev/netclaw) is ShellSyntaxTree's original +consumer. Its approval gate is a useful production case study, but its policy +choices are not part of ShellSyntaxTree's contract. + +The links below are immutable references to Netclaw commit +[`74014139a833050d777fbc913345904cca3b0544`](https://github.com/netclaw-dev/netclaw/commit/74014139a833050d777fbc913345904cca3b0544): + +- [Package reference and version pin](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/Netclaw.Security.csproj#L9-L12) + show the consumer dependency. That snapshot uses ShellSyntaxTree 0.1.5 and + therefore demonstrates the POSIX/Bash integration, not the newer PowerShell + parser. +- [Dependency-injection registration](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/SecurityServiceExtensions.cs#L33-L42) + binds `IShellParser` to `BashParser`. +- [Parser construction and safe-fail adaptation](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L200-L223) + supply the invocation working directory and convert unparseable or empty + results into "cannot decompose." +- [Approval-candidate extraction](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L225-L265) + evaluates every clause and keeps Netclaw's command-specific normalization in + the consumer. +- [Directory and redirect attribution](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L267-L345) + combine explicit operands, inherited cwd, and redirect targets. +- [Approval-unit grouping](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L347-L405) + starts new units for statements while retaining pipeline stages together for + display. +- [User-facing reconstruction](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L407-L465) + drops synthetic cwd attribution and applies product-specific summarization. +- [Fail-closed authorization](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security/IToolApprovalMatcher.cs#L549-L576) + refuses automatic approval when the parser cannot produce candidates. +- [Package integration canaries](https://github.com/netclaw-dev/netclaw/blob/74014139a833050d777fbc913345904cca3b0544/src/Netclaw.Security.Tests/ShellSyntaxTreeIntegrationTests.cs#L14-L182) + pin parser registration, verb extraction, compound splitting, cwd + attribution, and dynamic-content behavior across package upgrades. + +The reusable lesson is the flow: parse with real context, fail safely, inspect +every clause, keep derived cwd separate from authored tokens, and layer +application policy over parser facts. Netclaw's verb trimming, side-effect +classification, path predicate, and approval persistence are intentionally +application-specific. + +## Runnable samples + +The repository includes two public samples: + +- [`ShellSyntaxTree.Cli.Sample`](../samples/ShellSyntaxTree.Cli.Sample/) prints + the AST and applies a deliberately small audit policy. +- [`ShellSyntaxTree.Web.Sample`](../samples/ShellSyntaxTree.Web.Sample/) + renders Bash and PowerShell parses as Mermaid diagrams in the browser. + +The CLI policy is an illustration, not a production allow-list. It is useful +for seeing how a consumer walks paths, dynamic arguments, redirects, and +adjacent pipeline clauses. The Netclaw links above show how those primitives +fit into a real approval lifecycle. + +## Related contracts + +- [`SPEC.md`](../SPEC.md) defines the shared AST and Bash behavior. +- [`SPEC.POWERSHELL.md`](../SPEC.POWERSHELL.md) defines PowerShell-specific + parsing, alias resolution, parameter binding, and resolver behavior. +- [`README.md`](../README.md) provides installation and quick-start examples.