psadt-deploy-skill 0.26.7 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +957 -865
  2. package/bin/install.mjs +201 -125
  3. package/package.json +37 -37
package/README.md CHANGED
@@ -1,865 +1,957 @@
1
- <h1 align="center">PSADT v4 → Intune Deployment Skill</h1>
2
-
3
- <p align="center">
4
- <em>A Claude Code skill that drives the full lifecycle of a PowerShell App Deployment Toolkit (PSADT) v4.x Intune Win32 package — from first conversation to a tested, upload-ready <code>.intunewin</code>.</em>
5
- </p>
6
-
7
- <p align="center">
8
- <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" /></a>
9
- <img src="https://img.shields.io/badge/PSADT-v4.x-0a7bbb?style=flat-square" alt="PSADT v4.x" />
10
- <img src="https://img.shields.io/badge/Platform-Windows-0078d6?style=flat-square&logo=windows&logoColor=white" alt="Windows" />
11
- <img src="https://img.shields.io/badge/Claude%20Code-Skill-d97757?style=flat-square" alt="Claude Code Skill" />
12
- </p>
13
-
14
- <p align="center"><sub><a href="#quick-start">Quick start</a> · <a href="#how-it-works">How it works</a> · <a href="#features">Features</a> · <a href="#first-run-setup">Setup</a> · <a href="#roadmap">Roadmap</a> · <a href="#changelog">Changelog</a></sub></p>
15
-
16
- ---
17
-
18
- ## What is this?
19
-
20
- A **Claude Code skill** (not a plugin): a reusable instruction package that teaches the agent how to build,
21
- package, test, troubleshoot and deploy a **PSADT v4.x Intune Win32 app**. You describe the application; the
22
- skill runs the workflow — intake, web research, scaffolding, all three deployment types
23
- (Install / Uninstall / Repair), pre-flight checks, the SYSTEM test, packaging, the dossier, and the
24
- optional Graph upload.
25
-
26
- A skill is a folder with a `SKILL.md` (YAML frontmatter + Markdown instructions), here bundled with
27
- `scripts/` and `references/`. It loads progressively: the agent sees only the name and description until a
28
- task makes it relevant, then the full body loads on demand.
29
-
30
- <img width="1024" height="254" alt="image" src="https://github.com/user-attachments/assets/7c7931ba-dcae-4476-a648-11115eceb3b5" />
31
-
32
- ## Quick start
33
-
34
- ```powershell
35
- npx psadt-deploy-skill
36
- ```
37
-
38
- That installs the skill into `~/.claude/skills/psadt-deploy` and runs the setup doctor, which provisions
39
- everything it can and names the handful of values only you can supply (see
40
- [First-run setup](#first-run-setup)). Then open Claude Code in any folder and say what you want:
41
-
42
- > *"Create the Win32 Intune package for 7-Zip 24.09"* — or *"package Notepad++ for Intune"*
43
-
44
- The skill asks at most **four decision gates** (scope · deployment semantics · SYSTEM-test consent ·
45
- upload confirmation). Everything else it researches and states as an assumption instead of asking.
46
-
47
- ## How it works
48
-
49
- Twelve phases, each owned by a script rather than by prose, so a step either passed or did not:
50
-
51
- | Phase | What happens | Owner |
52
- |---|---|---|
53
- | **0** Setup | 13 prerequisite checks, GREEN/YELLOW/RED, `-Fix` provisions | `Initialize-PsadtSkill.ps1` |
54
- | **1–2** Intake + research | blocker questions as clickable options; parallel research of version, silent switches, Intune pitfalls | agent (gates 1–2) |
55
- | **3** Scaffold | a generator writes launcher + detection + per-run log name + manifest; `New-ADTTemplate` only when none fits | `New-MsiPackage` · `New-BrowserExtensionPackage` · `New-WindowsFeaturePackage` · `New-DriverPackage` |
56
- | **4** Customize | all three hooks filled from the research, helpers in the Extensions module | agent |
57
- | **5** Pre-flight | 10 checks (encoding, AST parse, v3 cmdlets, structure, detection contract, manifest, log name, driver trust …) → GREEN/RED | `Invoke-PsadtPreflight.ps1` |
58
- | **6** SYSTEM test | installs/uninstalls as **SYSTEM** like the IME does; **binding before any upload** | `Invoke-PsadtSystemTest.ps1` |
59
- | **7** Package | one command → verified `.intunewin`, named after the app | `Invoke-PsadtPackage.ps1` |
60
- | **8** Dossier | always, uploaded or not: bilingual self-contained HTML | `New-PsadtReport.ps1` |
61
- | **9** Upload *(opt-in)* | dry run → confirm → `win32LobApp` via raw Graph | `Invoke-IntuneWin32Upload.ps1` |
62
- | **10** Assignment *(opt-in)* | create/reuse Entra groups by naming scheme | `Invoke-IntuneAppAssignment.ps1` |
63
- | **11–12** Test + rollout | DEV-VM cycles, test group, pilot → staged production | agent |
64
-
65
- **Everything one app knows lives in `<pkg>\psadt-package.json`** — identity, the decisions taken at the
66
- gates, the research findings, every phase's result and the artifacts produced. The generators write it,
67
- every later phase reads and updates it, and pre-flight fails without it. That is what stops two packages of
68
- the same app from disagreeing about their own version.
69
-
70
- Depth lives in `references/PSADTv4-Deployment-Guide.md` (Phases 0–12 + Appendices A–Q); `SKILL.md` stays the
71
- control plane.
72
-
73
- ## Features
74
-
75
- ### Setup and prerequisites
76
-
77
- - **Setup doctor** — one idempotent script checks PowerShell 7, Windows PowerShell 5.1, elevation, git,
78
- PSAppDeployToolkit, the content-prep tool, `Invoke-CommandAs`, Pester, the config, a legacy config, the
79
- skill tree, a pending update and the Intune credentials. Each line carries a concrete fix; `-Fix` applies
80
- the ones that need no decision.
81
- - **Self-healing prerequisites** — installs the PSAppDeployToolkit module from the PowerShell Gallery and
82
- downloads `IntuneWinAppUtil.exe`, keeping both current against their official sources.
83
- - **Config outside the skill folder** — `config.json`, `secret.dpapi` and `tools/` live in
84
- `%LOCALAPPDATA%\psadt-deploy\` (override: `$env:PSADT_DEPLOY_HOME`), so a `git pull`, a re-clone or a
85
- re-install can no longer take your setup with it. A pre-0.19 config keeps working and is migrated on
86
- request, never deleted.
87
- - **One-line install** — `npx psadt-deploy-skill` (Node 18+, zero dependencies) does clone-or-update plus
88
- the doctor run in one step.
89
-
90
- ### Build and verify
91
-
92
- - **Guided intake** — the blocker questions up front as clickable options, pre-filled with researched
93
- defaults (app, latest version, installer type, package type).
94
- - **Autonomous research** — checks the installed PSADT version against the latest release *and* whether
95
- commands changed; researches silent install / uninstall / repair switches and known Intune pitfalls.
96
- - **All three deployment types from the start** — Install, Uninstall *and* Repair, acid-tested, so
97
- Company-Portal uninstalls actually work.
98
- - **Pre-flight gate** — encoding/BOM, AST parse, launcher acid test, v3-cmdlet scan, hook structure,
99
- detection-script contract, the package manifest, the per-run log name and driver trust. GREEN or RED,
100
- with the failing file named.
101
- - **Automated SYSTEM test loop** *(opt-in, binding before upload)* — installs, uninstalls and reinstalls as
102
- the **SYSTEM** account via `Invoke-CommandAs`, mirroring the Intune Management Extension; reads the fresh
103
- session log and the detection result, and hands back a structured verdict for the fix-and-retry loop.
104
- Needs an elevated session; belongs on a VM with a snapshot.
105
- - **Deterministic packaging** — `<outputRoot>\<Vendor>_<App>_<Version>_<Arch>\<same stem>.intunewin`,
106
- verified after the fact (`Detection.xml`, `SetupFile`, size, SHA256), with the detection script and the
107
- real logo beside it. It refuses an output folder inside the package, and never deletes a foreign
108
- `.intunewin` it finds there.
109
- - **One PSADT log per run** — `<Vendor>_<App>_<Version>_<Arch>_<Install|Uninstall|Repair>_<timestamp>.log`
110
- instead of every run of every version appending to one unreadable file.
111
-
112
- ### Package types
113
-
114
- The app's **native installer is always the default**. Everything else is opt-in and only on request:
115
-
116
- - **MSI / EXE** — the ordinary case, via `New-MsiPackage.ps1` or a hand-filled scaffold.
117
- - **WinGet** — the full `PSAppDeployToolkit.WinGet` lifecycle: the extension module self-heals into the
118
- package, the Package ID is discovered with `Find-ADTWinGetPackage`, hooks use `*-ADTWinGet*`
119
- (`-Scope Machine`). Never selected on its own initiative.
120
- - **Browser extensions** — force-install via the Edge/Chrome/Firefox policy keys (including the Firefox
121
- `REG_MULTI_SZ` trap), with selective removal on uninstall.
122
- - **Windows features** — `Enable-WindowsOptionalFeature` and `Add-WindowsCapability`, offline source or a
123
- temporary WSUS bypass that is restored afterwards, `EnablePending` handled honestly.
124
- - **Third-party drivers** — the trust situation is classified *before* anything is built: Microsoft-signed
125
- installs silently, vendor-signed needs the signer certificate owned in exactly one place, and a
126
- vendor-signed **kernel** driver is refused because `TrustedPublisher` satisfies the PnP prompt but never
127
- Code Integrity — it would install and then not load. Unsigned is refused outright, with three honest
128
- options and no testsigning. Staging is per-INF `pnputil`; uninstall resolves `oemNN.inf` by original name
129
- instead of a remembered index.
130
- - **Script-only / remediation packages** — ESP-safe patterns for fix packages with no installer at all.
131
-
132
- ### Deliverables
133
-
134
- - **HTML dossier — always generated**, uploaded or not. One self-contained file
135
- (`Intune-Dossier.html`) built from a fixed template, never hand-assembled: the **Intune dossier** (App
136
- Info, return-code map, detection rule, requirements, assignments, driver trust, and a ready-to-paste
137
- **Markdown** description for the Company-Portal field) plus a **technical package report** (the three
138
- hooks, PSADT cmdlets used, pre-flight and SYSTEM-test results, logo and `.intunewin` verification).
139
- Bilingual with a DE/EN toggle, browser-translatable, logo embedded as a data URI.
140
- - **Real logo only** — finds and downloads the actual application logo (vendor source or Wikimedia
141
- Commons), verifies real pixel transparency *and* looks at the image. The PSADT default `AppIcon.png` is
142
- blocked by hash.
143
- - **Start Menu only** — creates Start Menu entries and removes stray desktop icons.
144
-
145
- ### Intune
146
-
147
- - **Access as state, not as a 403** — `Test-PsadtIntuneAccess.ps1` answers before Phase 9 whether the app
148
- can upload, assign groups or create policies, and for how long the credential lives. Verified / refused /
149
- **unknown** are three different answers, and an offline check never overwrites what was verified before.
150
- - **Direct upload via Microsoft Graph** *(opt-in)* — pushes the `.intunewin` as a `win32LobApp` (app +
151
- logo), self-contained raw Graph, no third-party module. Identity comes from the manifest, so Intune shows
152
- the same name and version as the artifact and the dossier. Read-only dry run → confirm → upload. Fills the
153
- whole App-information tab, **never deletes an older version** (new versions coexist, with optional
154
- supersedence wiring), never auto-assigns categories or notes.
155
- - **One-time Entra bootstrap** — `New-PsadtEntraApp.ps1` signs in via **WAM**, creates the app, grants and
156
- admin-consents the roles and stores the credential: a **certificate** (preferred — nothing secret at rest,
157
- JWT client-assertion auth) or a DPAPI-encrypted client secret. Re-running it is normal: it finds the
158
- recorded app, merges requested permissions instead of replacing them, and never prompts.
159
- - **Opt-in group assignment** — creates/reuses Entra security groups by a configured naming scheme and
160
- assigns Required / Available / Uninstall. Least-privilege (`Group.Create` + `GroupMember.Read.All`),
161
- dry run → confirm, idempotent, and it never deletes a group or another app's assignment.
162
- - **Certificate + firewall policies** — Custom OMA-URI profiles for `TrustedPublisher` / `TrustedPeople`
163
- (the built-in template cannot reach those stores) and settings-catalog firewall-rule policies. Both
164
- scripts are self-contained deliverables: they can be copied to a test client that has no skill installed.
165
-
166
- ### Operations
167
-
168
- - **Troubleshooting** — decodes Intune error/HRESULT codes, maps symptoms to root causes, and triages the
169
- right log (`AppWorkload.log`, the PSADT session log, `setupapi.dev.log` for drivers).
170
- - **Self-update** — `scripts/Update-PsadtSkill.ps1` compares against GitHub, shows what changed, and
171
- updates in place on your confirmation (`git pull --ff-only` for a clone, otherwise a branch-zip overwrite
172
- of tracked files only). Machine-local state is never touched. Say *"update skill"* or *"psadt update"*.
173
- - **326 Pester tests** over the helper scripts, including drift guards that fail when the docs and the code
174
- disagree.
175
-
176
- ## Requirements
177
-
178
- - Windows with PowerShell 5.1+ / PowerShell 7+
179
- - For the `npx` installer only: **Node 18+** (the skill itself never needs Node)
180
- - [PSAppDeployToolkit](https://psappdeploytoolkit.com/) v4.x *(installed/updated automatically from the
181
- PowerShell Gallery if missing)*
182
- - [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
183
- *(provisioned automatically)*
184
- - For the **SYSTEM test loop**: an **elevated** session; the
185
- [`Invoke-CommandAs`](https://github.com/mkellerman/Invoke-CommandAs) module is installed automatically
186
- - For the **direct Intune upload**: an Entra app with the Graph application role
187
- `DeviceManagementApps.ReadWrite.All` (admin-consented) — created in one run by
188
- `scripts/New-PsadtEntraApp.ps1` (WAM sign-in as Global Admin / Privileged Role Admin, device-code
189
- fallback). Check what is actually in place with `scripts/Test-PsadtIntuneAccess.ps1`. Full permission
190
- matrix and the manual portal route: `references/app-registration.md`.
191
- - For **Pester tests**: Pester 5+ (`Install-Module Pester -MinimumVersion 5.0 -Scope CurrentUser`)
192
- - **Optional (recommended): the [superpowers](https://github.com/obra/superpowers) plugin** — if installed,
193
- the research fan-out and the reviewer gate use it. Not required: without it the skill falls back to the
194
- native Agent tool and `/code-review`, and nothing in the workflow depends on the plugin.
195
-
196
- ## Installation
197
-
198
- ```powershell
199
- npx psadt-deploy-skill
200
- ```
201
-
202
- Installs into `~/.claude/skills/psadt-deploy` and runs the setup doctor. Flags: `--dir <path>` ·
203
- `--project` (into `./.claude/skills`) · `--ref <branch|tag>` · `--no-setup`. Node 18+ and Windows; the
204
- installer itself has zero dependencies and the package carries only `bin/` — the skill is fetched from
205
- GitHub at install time.
206
-
207
- Re-running it updates an existing installation, and so does saying *"update skill"* to Claude Code
208
- (`git pull --ff-only` for a clone, otherwise a branch-zip overwrite of tracked files only — machine-local
209
- state is never touched).
210
-
211
- **Or clone it yourself** — the repo root *is* the skill folder:
212
-
213
- ```powershell
214
- git clone https://github.com/pt1987/claude-code-psadt-skill.git "$env:USERPROFILE\.claude\skills\psadt-deploy"
215
- pwsh "$env:USERPROFILE\.claude\skills\psadt-deploy\scripts\Initialize-PsadtSkill.ps1" -Fix
216
- ```
217
-
218
- `npx skills add pt1987/claude-code-psadt-skill` works too, since `SKILL.md` sits in the repository root.
219
-
220
- No git on the machine? The installer falls back to the branch tarball and Windows' own `tar.exe`, so the
221
- one-liner still works.
222
-
223
- The skill activates automatically when you ask Claude Code to build an Intune package, or when you work in
224
- a folder containing `Invoke-AppDeployToolkit.ps1`.
225
-
226
- ## First-run setup
227
-
228
- `scripts/Initialize-PsadtSkill.ps1` (also reachable by saying *"psadt setup"* / *"psadt doctor"*) checks
229
- every prerequisite in one pass and reports **GREEN / YELLOW / RED**. Every line comes with a concrete fix
230
- hint, and `-Fix` applies the ones that need no decision (module installs, the tool download, the
231
- `language.*` defaults, `paths.intuneWinAppUtil`, and migrating a pre-0.19 config). It is idempotent — run it
232
- as often as you like.
233
-
234
- Only four values genuinely need you; the doctor lists them in `.Missing` and takes them via `-Set`:
235
-
236
- ```powershell
237
- pwsh scripts/Initialize-PsadtSkill.ps1 -Fix -Set @{
238
- 'paths.packageRoot' = 'D:\Pakete'; 'paths.outputRoot' = 'D:\Intune'
239
- 'author.person' = 'Pat Taubert'; 'author.company' = 'PHAT Consulting'
240
- }
241
- ```
242
-
243
- | Setting | Purpose |
244
- |---|---|
245
- | `paths.packageRoot` / `outputRoot` | Where packages are built and where artifacts are written |
246
- | `paths.intuneWinAppUtil` | Content-prep tool location — filled by `-Fix` |
247
- | `language.script` / `dossier` | Script language (EN) vs. dossier language (DE for the Company Portal) — filled by `-Fix` |
248
- | `author.person` / `company` | Stamped into every package's `AppScriptAuthor` |
249
- | `intune.*` *(optional)* | Direct upload: tenant/client, credential reference, verified roles — written by `New-PsadtEntraApp.ps1` |
250
- | `intune.groups.*` *(optional)* | Opt-in group assignment (`enabled` / `create` / `membershipType` / `naming`) — guide Appendix M |
251
-
252
- ### Where the setup is stored
253
-
254
- `config.json`, `secret.dpapi` and `tools/` live in the **config home** — `%LOCALAPPDATA%\psadt-deploy\`,
255
- overridable with `$env:PSADT_DEPLOY_HOME` — **not** in the skill folder, so they survive a `git pull`, a
256
- re-clone and a re-install. They are machine-local and never committed. A `config.json` from a pre-0.19
257
- install (beside `scripts/`) keeps working read-only; the doctor flags it and `-Fix` migrates it, renaming
258
- the originals to `*.migrated` rather than deleting anything.
259
-
260
- > DPAPI is bound to the Windows user profile: a re-installed OS invalidates a stored client secret. The
261
- > doctor and `Test-PsadtIntuneAccess.ps1` both say so, and the fix is one `New-PsadtEntraApp.ps1` run.
262
-
263
- ## Project structure
264
-
265
- ```
266
- psadt-deploy/
267
- ├─ SKILL.md · README.md · CHANGELOG.md · LICENSE
268
- ├─ package.json · bin/install.mjs the npx installer (Node 18+, zero dependencies)
269
- ├─ scripts/
270
- │ │ setup + config
271
- │ ├─ Initialize-PsadtSkill.ps1 setup doctor (Phase 0, GREEN/YELLOW/RED, -Fix/-Set)
272
- │ ├─ Get-PsadtConfig.ps1 config read + config-home resolver
273
- │ ├─ Set-PsadtConfig.ps1 config write (deep merge, DPAPI secret, -Remove)
274
- │ ├─ Get-PsadtModule.ps1 PSADT module (self-heal)
275
- │ ├─ Get-IntuneWinAppUtil.ps1 content-prep tool (self-heal)
276
- │ ├─ Get-WinGetModule.ps1 WinGet extension (opt-in)
277
- │ ├─ Update-PsadtSkill.ps1 self-update from GitHub
278
- │ │ per-package truth
279
- │ ├─ Get-PsadtPackageManifest.ps1 manifest read (+ the artifact stem)
280
- │ ├─ Set-PsadtPackageManifest.ps1 manifest write (merge / append)
281
- │ │ package generators
282
- │ ├─ New-MsiPackage.ps1 MSI packages
283
- │ ├─ New-BrowserExtensionPackage.ps1 browser-extension force-install (opt-in)
284
- │ ├─ New-WindowsFeaturePackage.ps1 optional features / capabilities (opt-in)
285
- │ ├─ New-DriverPackage.ps1 driver packages, pnputil staging (opt-in)
286
- │ ├─ Get-DriverSignatureInfo.ps1 driver trust classifier (signed? kernel? deployable?)
287
- │ │ gates + deliverables
288
- │ ├─ Invoke-PsadtPreflight.ps1 pre-flight GREEN/RED gate (Phase 5, 10 checks)
289
- │ ├─ Invoke-PsadtSystemTest.ps1 SYSTEM test (Phase 6)
290
- │ ├─ Invoke-PsadtPackage.ps1 build the .intunewin (Phase 7, named + verified)
291
- │ ├─ New-PsadtReport.ps1 HTML dossier (Phase 8, always)
292
- │ │ intune / graph
293
- │ ├─ New-PsadtEntraApp.ps1 Entra app bootstrap (WAM)
294
- │ ├─ Get-GraphToken.ps1 app-only Graph token (cert / DPAPI)
295
- │ ├─ Test-PsadtIntuneAccess.ps1 access verdict (roles, capabilities, expiry)
296
- │ ├─ Invoke-IntuneWin32Upload.ps1 direct upload (Phase 9)
297
- │ ├─ Invoke-IntuneAppAssignment.ps1 group assignment (Phase 10, opt-in)
298
- │ ├─ New-IntuneTrustedCertPolicy.ps1 Custom OMA-URI cert policy (self-contained)
299
- │ ├─ New-IntuneFirewallPolicy.ps1 firewall-rule policy (self-contained)
300
- │ ├─ _GraphCommon.ps1 shared Graph helpers (retry, errors, token roles)
301
- │ └─ _GraphInteractive.ps1 shared WAM sign-in
302
- ├─ references/
303
- │ ├─ PSADTv4-Deployment-Guide.md Phases 0-12 + Appendices A-Q
304
- │ ├─ Report-Template.html the fixed dossier template
305
- │ └─ app-registration.md THE Graph permission matrix + manual portal route
306
- └─ tests/ Pester suite, 326 tests
307
- ```
308
-
309
- Machine-local state lives outside the skill folder:
310
-
311
- ```
312
- %LOCALAPPDATA%\psadt-deploy\ ($env:PSADT_DEPLOY_HOME overrides)
313
- ├─ config.json settings incl. the optional intune.* block
314
- ├─ secret.dpapi DPAPI client secret (only without cert auth)
315
- └─ tools/ IntuneWinAppUtil.exe + WinGet module
316
- ```
317
-
318
- And per package, next to `Invoke-AppDeployToolkit.ps1`:
319
-
320
- ```
321
- psadt-package.json identity · gate decisions · research · results · artifacts
322
- ```
323
-
324
- ## Status
325
-
326
- In active use for the full build → package → test → dossier workflow, with the direct Graph upload
327
- verified against a live tenant. The helper scripts are covered by 326 Pester tests.
328
-
329
- One open point, honestly: **the driver `pnputil` exit-code semantics are documented, not verified here.**
330
- `0` / `259` / `3010` and the two `0xE...` failures come from Microsoft's documentation; confirming them
331
- against `setupapi.dev.log` on a DEV VM with a real vendor-signed and a real Microsoft-signed driver is
332
- still open.
333
-
334
- ## Roadmap
335
-
336
- Designed and waiting to be built:
337
-
338
- - **Sync finished packages to a GitHub repo** — a setup option (`output.target` = `local` / `git` / `both`)
339
- to push the per-app artifacts (`.intunewin`, dossier, detection, logo) to a Git repo instead of, or in
340
- addition to, a local folder — versioned and shareable. Will need **Git LFS** for large `.intunewin` files
341
- (GitHub's 100 MB per-file limit).
342
-
343
- Have a request? Open an issue.
344
-
345
- ## Contributing
346
-
347
- Issues and pull requests are welcome. Keep `SKILL.md`, the references and the docs in **English**. The only
348
- non-English content is the generated end-user output (the Intune dossier and the Company-Portal app
349
- description), whose language follows the `language.dossier` config value — **default German**, but
350
- configurable per machine.
351
-
352
- Two conventions worth knowing before you send a patch: generated `.ps1` content is **7-bit ASCII** (the
353
- pre-flight fails on non-ASCII without a BOM), and anything that lands in a package's output folder must be
354
- **self-contained** — it gets copied to test clients that have no skill installed.
355
-
356
- ## License
357
-
358
- [MIT](LICENSE) © Patrick Taubert, PHAT Consulting GmbH
359
-
360
- ## Acknowledgements
361
-
362
- - [PSAppDeployToolkit](https://github.com/PSAppDeployToolkit/PSAppDeployToolkit)
363
- - [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
364
- - [`Invoke-CommandAs`](https://github.com/mkellerman/Invoke-CommandAs)
365
- - README structure inspired by [ComposioHQ/awesome-claude-skills](https://github.com/ComposioHQ/awesome-claude-skills)
366
-
367
- ## Changelog
368
-
369
- Recent releases below; the complete history is in **[CHANGELOG.md](CHANGELOG.md)** (nothing is ever removed
370
- from either).
371
-
372
- ### 0.26.7 - 08.09.2026
373
- - **Fixed: the control plane did not know MSIX exists.** L.8/L.9 landed in 0.26.5, but Gate 1's package-type list
374
- never mentioned MSIX — so a `.msix` fell through to "native installer (default)", the one route that is wrong for
375
- it, and nothing pointed at the new appendix. Gate 1 now carries **MSIX/AppX** with the decision attached: Intune
376
- takes it **natively as a line-of-business app**, so the default is to use that and *not* build a PSADT package;
377
- the wrapper is the exception, and L.8 is read first when it is used.
378
- - **Added `tests/SKILL.Tests.ps1`**, a coherence guard: every appendix SKILL.md points at must exist, and the Gate 1
379
- decision must actually route MSIX. Research that the router cannot reach is dead weight — now a test failure.
380
- ### 0.26.6 - 08.09.2026
381
- - **Fixed: the sandbox host timeout force-killed the viewer, then told you to close the window.** A timed-out run
382
- was torn down like a finished one. That **orphans `vmmemWindowsSandbox`** — the Hyper-V compute service owns it,
383
- so the host cannot kill it, and it keeps the work folder locked until the next run throws on it — and it removes
384
- the only window through which the guest could still be shut down cleanly. The script's own comments name this
385
- mechanism as the reason the *guest* must shut itself down; the timeout path did it anyway. A timeout now touches
386
- nothing and explains what to close and which knob to raise.
387
- - **Fixed: the warning claimed 60 seconds while the code waited 180** — a literal duplicating a parameter default.
388
- One declared value now feeds both.
389
- - **Appendix G finding 6 corrected**: "no recovery path" is now scoped to a run killed from *outside* the script.
390
- ### 0.26.5 - 08.09.2026
391
- - **L.8, new: MSIX/AppX**, verified against Microsoft Learn and against the live cmdlets. MSIX is a two-step
392
- model — machine-wide **staging**, then **per-user registration at logon** — and three traps follow from it:
393
- **`Add-AppxPackage` as SYSTEM registers the app for the SYSTEM account and reports success** (nobody can launch
394
- it), **`Get-AppxPackage` is the wrong detection cmdlet** (finds nothing right after provisioning, so Intune
395
- reinstalls forever), and **de-provisioning does not remove the app from existing users** — a complete uninstall
396
- needs `Remove-AppxProvisionedPackage` *and* `Remove-AppxPackage -AllUsers`. Plus signing: the certificate
397
- Subject must equal the manifest Publisher (so a vendor MSIX cannot just be re-signed), self-signed certs belong
398
- in `LocalMachine\TrustedPeople`, and missing timestamping is what makes an expired certificate break installs.
399
- And the decision that comes first: **usually don't wrap MSIX in PSADT** — Intune takes it natively.
400
- - **L.9, new: App-V is not end of life.** The client and sequencer are **no longer deprecated** (fixed extended
401
- support, no end date, no extra cost); only the **server components** end in **April 2026**. Also: `-Global` is
402
- the device-context switch, and a package in use goes *pending* — a global task applies only after a restart.
403
- - **The MSIX row in L.2 was misleading and is rewritten**; L.2 gains an App-V row.
404
- ### 0.26.4 - 08.09.2026
405
- - **L.7, new: Inno Setup and NSIS**, verified against vendor documentation. `/VERYSILENT` **reboots the
406
- machine by itself** when a restart is needed — so `/NORESTART` is mandatory, and `/RESTARTEXITCODE=3010`
407
- turns the fact into the code Intune already understands. And `Uninstall.exe /S` in NSIS **returns before
408
- the uninstall has finished** (it copies itself to temp and detaches), so any verification afterwards races
409
- it — `_?=<installdir>` makes it synchronous, must be last, must not be quoted.
410
- - **Advanced Installer wired into L.1 and L.2**, not just the new L.6 — with a measured fingerprint
411
- (`AI_*` custom actions, `aicustact.dll`, `OLDPRODUCTS;AI_NEWERPRODUCTFOUND`).
412
- ### 0.26.3 - 08.09.2026
413
- - **Appendix L gains three sections**, researched against vendor documentation instead of written from
414
- memory: **MSP patches** (the full `/p` matrix, why `/i` and `/p` never combine, and that exit 1642 means
415
- *either* "not installed" *or* "wrong patch revision" — so it must not be treated as a blanket success),
416
- **WiX Burn bundles** (BundleProviderKey rather than a ProductCode for detection; `/layout` is a Burn
417
- action but the bootstrapper decides whether it can be narrowed — the ADK refuses, so an offline layout is
418
- always the whole kit), and **Advanced Installer projects** (the `.aip` CLI, a fresh ProductCode per build
419
- changing the package identity every time, and the undocumented behaviour that relative paths resolve
420
- against the .aip's own location — moving a project between drives breaks it silently).
421
- ### 0.26.2 - 08.09.2026
422
- - **Fixed: a WHQL driver pack turned the pre-flight red.** `Get-DriverSignatureInfo.ps1` did not strip the
423
- inline comment that WHQL packs write after `CatalogFile=` (`; for WHQL certified`), so the catalog never
424
- resolved and validly signed drivers classified as `Unsigned` — 6 of 70 INF in a Dell WinPE set, all of
425
- whose catalogs were Authenticode-valid and Microsoft-signed. An unquoted `;` now ends the value.
426
- - **Fixed: `New-MsiPackage.ps1` was the sixth script with the `-File` binder trap.** `-ProcessesToClose
427
- 'a','b'` arrived as one element, so the scaffold closed no processes at all — silently, while reporting
428
- success. 0.25.1 had fixed the other five and missed the generator.
429
- - **Documented: seven findings from a three-package dependency chain** (Appendix G) and four new
430
- anti-patterns (Appendix B), including `msiexec /a` against bundled payload, the `-Include` trap, 8.3
431
- short names in path comparisons, and what a service's `%TEMP%` really is on Windows 11.
432
- ### 0.26.1 - 06.09.2026
433
- - **Fixed: a syntax error in the template silently killed the whole dossier script.** A `
434
- ` inside a JS
435
- string literal became a real newline; JavaScript then discards the entire `<script>` block, so the
436
- language toggle, all copy buttons and the condensing sticky header died at once — while all 419 tests
437
- stayed green, because they only assert on rendered HTML. `tests/Report-Template.Tests.ps1` now runs
438
- `node --check` on the script block plus a node-free quote-balance check, both proven against the
439
- reintroduced bug.
440
- - **Changed: every table copies by clicking the row**, replacing the hover icon. Key/value tables give the
441
- value, return codes the code, assignments the group name, the SYSTEM-test table the whole row. Clicks on
442
- a row's own controls (the foldable detection script) and text selections are left alone. The Markdown
443
- description keeps its own copy button.
444
-
445
- ### 0.26.0 - 06.09.2026
446
- - **Fixed: the dossier could name a return-code type Intune does not have.** Graph accepts exactly
447
- `success`, `softReboot`, `hardReboot`, `retry`, `failed` — there is no `ignored`, yet "Ignored" reached a
448
- real dossier because the report rendered a caller-supplied table without validating anything (`Label` was
449
- free text, `Cls` was interpolated raw into a `class` attribute). `Label` and `Cls` are now derived from
450
- `Type` through a closed switch and cannot be supplied at all; an invalid type throws.
451
- - **Return codes are sorted and copyable.** Sorted by type in Appendix F.4 order, then numerically — the
452
- order the table is verified against and typed into the portal. Each code cell has a copy icon and the
453
- section a "copy table" button.
454
- - **New `Get-PsadtReturnCodes.ps1`: one source of truth for the dossier AND the upload.** Previously two
455
- independent literals that agreed by coincidence. `Invoke-IntuneWin32Upload.ps1` gained `-ReturnCodes`, so
456
- installer-specific codes finally reach the app instead of only the document, and a new manifest key
457
- `research.returnCodes` records them once for both.
458
- - 419 tests; six new regression guards, each verified to fail on its reintroduced bug.
459
-
460
- ### 0.25.2 - 06.09.2026
461
- - **Fixed: a sandbox run left an error dialog on the desktop.** The guest shuts itself down at the end, the
462
- RDP-style client loses its session and shows a connection-lost box. The host now disposes of the viewer as
463
- soon as the run reports in. The tempting fix — let the host kill the VM instead of shutting the guest down
464
- — was measured and is wrong: it orphans the `vmmemWindowsSandbox` worker, which then holds the work folder
465
- open for minutes and cannot be killed. Guest shuts down, host closes the window, and the wait is on the VM
466
- worker rather than on the client processes.
467
-
468
- ### 0.25.1 - 06.09.2026
469
- - **Fixed: array parameters broke under the `-File` binder.** `pwsh script.ps1 -Intents a,b` passes the
470
- whole string as ONE element, so `Invoke-IntuneAppAssignment.ps1` rejected a perfectly valid comma-separated
471
- intent list with a ValidateSet error naming a value nobody typed. It now splits the list itself and still
472
- rejects a real typo by name. `Invoke-PsadtSandboxTest.ps1 -Paths*` had the same trap with a worse outcome —
473
- only the first path was asserted and the run still reported GREEN. Six regression tests; suite at 382.
474
-
475
- ### 0.25.0 - 05.09.2026
476
- - **New: `Get-PsadtMsiFacts.ps1` reads an MSI in one pass** — identity, signature, SHA256, features with
477
- component counts, *decoded* upgrade flags, shortcuts, directories, file versions, registry rows and the
478
- Icon table. For an MSI this probe **is** the research: it is what reveals an auto-updater sitting in its
479
- own feature (so `ADDLOCAL` replaces post-install cleanup), a `DesktopFeature` you must not install, and
480
- `MigrateFeatures` on the upgrade row — the flag that means `ADDLOCAL` alone is not enough. Five
481
- regression guards, each verified to fail on its reintroduced bug.
482
- - **Fixed: the sandbox test left litter and put the evidence in the wrong place.** It kept one work folder
483
- per app under the config home forever, and `result.json` plus the PSADT logs lived there with the
484
- manifest pointing into a profile directory. Evidence now lands in
485
- `<outputRoot>\<Stem>\SandboxTest\` beside the dossier — not in the package folder, which is what
486
- IntuneWinAppUtil packs — the logs are recorded in `artifacts.logs[]`, and the scratch is removed.
487
- `-KeepWorkFolder` keeps it, and a failed run keeps it automatically.
488
- - **Fixed: the test suite wrote into the real config home.** It now redirects `PSADT_DEPLOY_HOME` to a temp
489
- directory, with a test that asserts it.
490
- - **Measured:** PuTTY 0.85 packaged with these lessons applied took **13 min 45 s end to end** — versus 75
491
- minutes for the Notepad++ run that produced them. The largest single factor: the sandbox test costs zero
492
- wall-clock time when started as soon as pre-flight goes GREEN, because packaging and the dossier do not
493
- depend on its verdict.
494
-
495
- ### 0.24.0 - 05.09.2026
496
- - **New: `Invoke-PsadtSandboxTest.ps1` runs the whole Phase 6 loop in a throwaway Windows Sandbox** —
497
- Install, detection, Uninstall, detection, Reinstall, Repair, final Uninstall, every action as
498
- `NT AUTHORITY\SYSTEM` via a scheduled task. **No elevation on the host, and the host is never touched**,
499
- so the SYSTEM test stops being the step that gets deferred to "a DEV VM later". Every action starts from
500
- a machine that has never seen the app, so a pass cannot be an artefact of the previous run. The verdict
501
- is keyed on the detection script; package-specific facts go in via `-PathsPresentAfterInstall` /
502
- `-PathsAbsentAfterInstall` / `-PathsAbsentAfterUninstall`. Verified end to end on Notepad++ 8.9.8 x64:
503
- GREEN, all seven steps, 5 min 58 s.
504
- - **Why it is a script and not a snippet:** a hand-rolled version hit three bugs that each destroyed a full
505
- VM run and each present as a timeout or a null-reference minutes after launch — `echo %ERRORLEVEL%>file`
506
- silently becoming the `0>` stdin redirection (empty file, never a number), file existence read as
507
- completion, and `[string]$null` still being `$null` in Windows PowerShell 5.1 (an empty detection output
508
- is the *normal* result, so the harness crashed because the package was clean). All three now have
509
- regression guards that were each verified to fail on the reintroduced bug.
510
- - Phase 6 in SKILL.md and the guide now lead with the sandbox route; the per-action route stays for apps a
511
- VM cannot host. New anti-patterns cover hand-rolled harnesses, debugging through a long job instead of a
512
- two-second local check, and "speeding up" the test by disabling Defender or dropping Uninstall/Repair.
513
-
514
- ### 0.23.1 - 04.09.2026
515
- - **Fixed: two generators were broken since 0.21.0.** `New-BrowserExtensionPackage.ps1` and
516
- `New-WindowsFeaturePackage.ps1` died at run time — the per-run `LogName` change had wedged a statement
517
- into the template `Replace(...)` chain, which PowerShell parses as a property assignment with an empty
518
- name. Valid syntax, so neither the parse check nor the source-inspecting tests could see it; it surfaced
519
- the first time the whole chain ran against a real package. A guard in all four generator test suites now
520
- fails on a broken chain, and the guard itself was verified against a broken and a good sample.
521
-
522
- ### 0.23.0 - 04.09.2026
523
- - **`npx psadt-deploy-skill`.** One line installs or updates the skill into `~/.claude/skills/psadt-deploy`
524
- and runs the setup doctor. Flags `--dir`, `--project`, `--ref`, `--no-setup`. Cloning to exactly the right
525
- path by hand was the first thing a new user could get wrong.
526
- - **Zero dependencies** (Node 18's `fetch` + the `tar.exe` Windows ships). Three acquisition routes:
527
- update an existing clone, else `git clone --depth 1`, else the branch tarball — because plenty of managed
528
- machines have no git. The npm package ships only `bin/`; the skill comes from GitHub at install time, so
529
- a new skill version needs no republish.
530
- - **The installer never writes the config itself** — it spawns `Set-PsadtConfig.ps1` and
531
- `Initialize-PsadtSkill.ps1 -Fix`, because a second config-home implementation in JavaScript would drift
532
- from `Get-PsadtConfig`. `Update-PsadtSkill.ps1` now tracks `package.json` and `bin/` so the archive
533
- update route stops dropping them. Suite 307 → 326 tests.
534
-
535
- ### 0.22.0 - 04.09.2026
536
- - **Third-party drivers.** New `scripts/Get-DriverSignatureInfo.ps1` classifies a driver folder before
537
- anything is built — Microsoft-signed / vendor-signed / unsigned — by checking the **catalog** signature
538
- rather than the `.sys` (a dual-signed `.sys` reports only its primary signature). New
539
- `scripts/New-DriverPackage.ps1` builds the package: pnputil staging per INF, uninstall that resolves
540
- `oemNN.inf` by original name instead of a remembered index, `Get-WindowsDriver` detection.
541
- - **The rule that saves the most time:** a vendor-signed *kernel* driver is RED, not a warning. With Secure
542
- Boot on, only Microsoft Dev-Portal-signed kernel drivers load — importing the signer certificate removes
543
- the "install device software?" prompt but does nothing for Code Integrity, so the driver installs and
544
- then never loads. Unsigned drivers are refused outright, with three honest options and no testsigning.
545
- - Pre-flight gained a `DriverTrust` check that fires for **any** package shipping an `.inf` (a vendor
546
- installer staging a driver is the case nobody declares), the dossier has a driver-trust row, and
547
- `New-IntuneTrustedCertPolicy.ps1` is now fully self-contained like the firewall script.
548
- - New guide **Appendix Q** with the decision tree, pnputil exit codes and the installer-bundled-driver
549
- case. Suite 251 → 307 tests.
550
-
551
- ### 0.21.0 - 04.09.2026
552
- - **Every package gets a manifest** (`psadt-package.json`): identity, gate decisions, research findings,
553
- every phase result and the produced artifacts, in one file per app. The generators write it, every phase
554
- script reads and updates it — so an app's version, name and test evidence stop living in someone's head.
555
- - **The `.intunewin` is named after the app.** New: `<outputRoot>\<Vendor>_<App>_<Version>_<Arch>\<same
556
- stem>.intunewin`, produced by the new `scripts/Invoke-PsadtPackage.ps1`. Until now every package came out
557
- as `Invoke-AppDeployToolkit.intunewin` — that name reached Intune, and concurrent uploads collided in one
558
- shared temp folder. The new script also verifies the archive before calling it a deliverable and refuses
559
- an output folder inside the package (which made the tool pack its own previous output).
560
- - **One PSADT log per run** instead of one ever-growing file:
561
- `<Vendor>_<App>_<Version>_<Arch>_<Install|Uninstall|Repair>_<timestamp>.log`. PSADT appends to a fixed
562
- name by default, so by the third attempt a failed install was unreadable.
563
- - **Pre-flight gained two checks:** a missing or incomplete manifest is RED, a pre-0.21 launcher without a
564
- per-run log name is a WARN. Report and upload both take their identity from the manifest.
565
- - Guide: the missing `Phase 6 / 9 / 10` sections now exist, and Appendix E is numbered by phase.
566
- Suite 173 → 251 tests.
567
-
568
- ### 0.20.0 - 04.09.2026
569
- - **Intune access is state, not a 403.** New `scripts/Test-PsadtIntuneAccess.ps1` answers *before* Phase 9
570
- whether the configured app can actually upload, assign groups or create policies — and for how long the
571
- credential lives. `TokenOk` and every capability are three-valued: verified / refused / **unknown**,
572
- because Graph tokens are opaque by contract and "we could not tell" is not "not permitted". An offline run
573
- never overwrites what was verified earlier.
574
- - **The scripts assert the role they need before their first write** instead of discovering it from a 403
575
- mid-upload. Group assignment requires *both* group roles and names the missing half.
576
- - **`New-PsadtEntraApp.ps1` is re-runnable.** It finds the app by the recorded `clientId`, **merges**
577
- requested permissions instead of replacing them (a run without `-IncludeConfigurationManagement` used to
578
- silently revoke that role), never prompts, persists what it learned, and only sets `uploadEnabled` once
579
- consent is really in place. Older client secrets are counted, never deleted.
580
- - **Auth failures say what to do:** expired/invalid client secret, unknown app or tenant, Conditional Access
581
- block — and an undecryptable DPAPI secret now explains that DPAPI is bound to the Windows user profile.
582
- - `references/app-registration.md` is now the single permission matrix (app roles → capabilities → how to
583
- grant), referenced from the guide instead of duplicated. Suite 128 → 173 tests.
584
-
585
- ### 0.19.0 - 04.09.2026
586
- - **Setup doctor: `scripts/Initialize-PsadtSkill.ps1`.** One idempotent script replaces the Phase 0 prose
587
- wizard and reports GREEN/YELLOW/RED over 13 prerequisite checks, each with a concrete fix hint. `-Fix`
588
- installs the modules, downloads the content-prep tool, fills the EN/DE + tool-path defaults and migrates an
589
- old setup; `-Set @{...}` persists your values first; `-Json` / `-JsonPath` for other tooling. `.Missing`
590
- lists only the four values a human has to supply (`paths.packageRoot`, `paths.outputRoot`, `author.person`,
591
- `author.company`) — never a key the doctor could fill itself.
592
- - **Config, secret and tools moved to a per-user config home** (`%LOCALAPPDATA%\psadt-deploy\`, override
593
- `$env:PSADT_DEPLOY_HOME`) instead of the skill folder, so a `git pull`, re-clone or re-install no longer
594
- takes the whole setup with it — and scripts started from an output folder still find their config.
595
- `Get-PsadtConfig.ps1` is the single resolver and now returns `.Home` / `.DefaultHome` / `.LegacyInUse`. An
596
- old config beside `scripts/` keeps working read-only until `-Fix` migrates it (originals renamed
597
- `*.migrated`, nothing deleted).
598
- - **`Set-PsadtConfig.ps1 -Remove`** deletes dotted keys, so switching credential type can clean up the
599
- stale one. **Fixed:** `New-PsadtEntraApp.ps1` reported `<skill>\config.json` even when the config lived
600
- elsewhere. Suite 120 → 128 tests.
601
-
602
-
603
- <details>
604
- <summary><strong>Earlier releases (0.18.1 and older)</strong></summary>
605
-
606
- ### 0.18.1 - 03.09.2026
607
- - **Upload: `-MaxRunTimeMinutes`.** `Invoke-IntuneWin32Upload.ps1` can now set
608
- `installExperience.maxRunTimeInMinutes` (1–1440); `0` (default) omits the field and keeps the service default
609
- of 60 min. Raise it for long-running installs (OS in-place upgrades, large suites) so the IME does not kill them.
610
-
611
- ### 0.18.0 - 01.07.2026
612
- - **HanseMerkur corporate design + editorial report redesign.** The dossier/report template is re-themed to the
613
- HanseMerkur CD (green brand family on a light mint canvas; Metric font stack with Segoe fallback and no
614
- web-font fetch → no CORS console errors on a local `file://` open) and relaid out as an "editorial
615
- data-report": flat hairline sections, auto-numbered headings (`01…13`), an at-a-glance KPI band under the hero
616
- (version · pre-flight · min OS · arch), and a wider 1600px layout. The detection script is folded behind a
617
- collapsed `<details>` (the rule summary stays visible). German report text now uses real umlauts. Fixed the
618
- sticky-header flicker (Chrome/Edge scroll-anchoring vs. the condensing hero → `overflow-anchor: none`,
619
- Playwright-verified) and removed the redundant hero status pill.
620
-
621
- ### 0.17.0 - 01.07.2026
622
- - **install4j fingerprint + behavioral silent-switch verification.** Appendix L.1 now recognises install4j
623
- (Java) installers (`com/install4j/runtime`, `exe4j`, `i4jparams.conf`, bundled `jre\`) and records that `/S`
624
- is NOT its switch (it hangs on the language dialog) — the unattended switch is `-q`, run elevated. New BINDING
625
- rule: a single string match is a hint, not proof; confirm the engine by its definitive fingerprint AND run the
626
- silent switch once (timeout+kill, expect exit 0, no dialog) before packaging. Appendix L.3 adds the
627
- trademark-sign gotcha (`Name(R)` breaks `-match 'Name'` → tolerant regex); Appendix B adds anti-patterns 13–15.
628
- (Driven by an Aperio install4j installer misidentified as NSIS, where `/S` hung on the language dialog.)
629
-
630
- ### 0.16.0 - 29.06.2026
631
- - **Dossier auto-sync convention (BINDING)** + report header layout fix. Any change to the package scripts
632
- (launcher, Extensions, detection, version/changelog, return codes, re-packaging) now requires regenerating
633
- `Intune-Dossier.html` in the same pass; a stale dossier is a defect. The `.pill-lg` status badge caps at 230px
634
- and wraps so a long status no longer overlaps the hero title.
635
-
636
- ### 0.15.2 - 15.06.2026
637
- - **Follow-up doc fix.** A contradiction sweep after 0.15.1 caught one more stale "Phase 7.5" in
638
- `New-PsadtReport.ps1` help (upload is Phase 9); corrected. No other live stale references remain.
639
-
640
- ### 0.15.1 - 15.06.2026
641
- - **Generator hardening from a self-review (correctness + security).** All three generators now single-quote-escape
642
- values embedded in `$adtSession` literals, so an apostrophe in the App name/vendor/author (e.g. "Bob's App",
643
- "L'Oreal") no longer produces an unparseable package; the MSI `-AdditionalArgumentList` / `ProcessesToClose`
644
- literals are escaped too (also closing a SYSTEM code-injection path). Detection exit-code drift fixed:
645
- `New-MsiPackage.ps1` + the WinGet example now `exit 0` for "not installed" (a non-zero exit reads as a detection
646
- error), and a new **pre-flight Detection check** WARNs on a non-zero exit in `Detect*.ps1`. The WSUS bypass in
647
- `New-WindowsFeaturePackage.ps1` now saves all prior state before writing and runs inside the `try/finally`, so a
648
- partial failure can't leave `UseWUServer=0` permanently. Added input guards (`$Name` path-traversal, `__TOKEN__`
649
- leak), MSI `-Author` config fallback + `-InstallerPath` validation. Stale refs fixed (SKILL.md "A-M"->"A-P",
650
- `New-PsadtEntraApp.ps1` "Phase 7.5"->"Phase 9").
651
-
652
- ### 0.15.0 - 15.06.2026
653
- - **Windows-feature packages (optional features + capabilities / FoD).** New `scripts/New-WindowsFeaturePackage.ps1`
654
- — one-call generator that enables Windows **Optional Features** (`Enable-WindowsOptionalFeature`: NetFx3,
655
- Hyper-V, WSL, TelnetClient, …) and **Capabilities / Features on Demand** (`Add-WindowsCapability`: RSAT.*,
656
- OpenSSH, …) from one typed list, multiple per package. Uninstall reverts (disable/remove); Repair re-enables
657
- (idempotent). Reboot surfaces **3010** via `$adtSession.SetExitCode(3010)` (`-NoRestart`); detection treats
658
- `EnablePending` as not-yet-done. Content comes from a bundled `-Source` (offline SxS) else Windows Update
659
- behind a **temporary** WSUS bypass (`RepairContentServerSource=2`, `UseWUServer=0`) whose exact prior state is
660
- restored. Guide **Appendix P**, SKILL.md Gate-1/anti-patterns/ref-lookup. Pre-flight GREEN; helper logic
661
- verified against an in-memory registry sim; ASCII-clean.
662
-
663
- ### 0.14.0 - 15.06.2026
664
- - **Browser-extension force-install packages (Edge / Chrome / Firefox).** New
665
- `scripts/New-BrowserExtensionPackage.ps1` — one-call generator for force-installing browser extensions via
666
- enterprise **policy registry keys** (policy-only, no installer, ESP-safe). Multiple extensions per package.
667
- Chromium helper computes the **next free `ExtensionInstallForcelist` index** (never hard-codes `1`), dedupes by
668
- ID and removes only its own entry (coexistence); Firefox merges into the single `ExtensionSettings` JSON written
669
- as **`REG_MULTI_SZ`** (single-line `REG_SZ` is silently ignored — Mozilla bug 1750233). Guide **Appendix O**,
670
- SKILL.md Gate-1/anti-patterns/ref-lookup. Pre-flight GREEN; helpers validated against a scratch registry hive;
671
- ASCII-clean.
672
-
673
- ### 0.13.1 - 15.06.2026
674
- - **Firewall policy body fixed against the live template (verified 201).** `New-IntuneFirewallPolicy.ps1`
675
- produced a body Graph rejected (400). Corrected via the **msgraph skill** (not guessed): the group id needs
676
- the `{firewallrulename}` token, the program path is the direct child `..._app_filepath`, action values are
677
- `_action_type_1`/`_0`, and a template-based policy requires `settingInstanceTemplateReference` per instance +
678
- `settingValueTemplateReference` per simple/choice value (profiles collection: instance ref only — a per-value
679
- ref is a duplicate). Confirmed by a live **201 Create**; tests assert the references. Mirrored into the
680
- MxManagementCenter Output deliverable.
681
-
682
- ### 0.13.0 - 15.06.2026
683
- - **Self-contained firewall deliverable (copy-to-client safe).** `scripts/New-IntuneFirewallPolicy.ps1` is now
684
- fully self-contained — no dot-sourcing of `_GraphCommon`/`_GraphInteractive`, no skill path; WAM sign-in +
685
- policy body builder + console helpers are embedded. It runs on a test client that does **not** have the skill
686
- installed (`-Interactive` WAM, or `-GraphToken`). Fixes the "Skill script not found … -SkillRoot" failure when
687
- the deliverable was copied to another machine. New **binding SKILL.md convention "Self-contained deliverables"**
688
- + a Pester test that enforces it (no dot-source / no skill path / embeds WAM). Test-first per writing-skills.
689
-
690
- ### 0.12.0 - 15.06.2026
691
- - **Interactive WAM sign-in for the Intune policy scripts.** `New-IntuneFirewallPolicy.ps1` and
692
- `New-IntuneTrustedCertPolicy.ps1` gain `-Interactive` (+ `-TenantId`): delegated sign-in via **WAM**
693
- (Windows Web Account Manager) when there is no app registration — **no device code**. The WAM machinery
694
- (`Initialize-MsalBroker` / `Get-WamToken` / new `Get-InteractiveGraphToken`) was extracted from
695
- `New-PsadtEntraApp.ps1` into a shared `scripts/_GraphInteractive.ps1` (one implementation, no copy-paste
696
- drift); the bootstrap now consumes it. The MxManagementCenter firewall deliverable became a thin wrapper
697
- over the generic script.
698
-
699
- ### 0.11.0 - 15.06.2026
700
- - **Intune firewall-rules policy + app config-management permission.** New `scripts/New-IntuneFirewallPolicy.ps1`
701
- (Endpoint Security "Windows Firewall Rules" policy, one program-scoped rule; dry-run / `-Execute` / manual
702
- portal fallback) + Pester test. Suppresses the first-run Windows Firewall prompt for apps that listen inbound.
703
- New `New-PsadtEntraApp.ps1 -IncludeConfigurationManagement` consents `DeviceManagementConfiguration.ReadWrite.All`
704
- (needed by the firewall and trusted-cert policies for `-Execute`).
705
-
706
- ### 0.10.0 - 15.06.2026
707
- - **Certificate store deployment (driver-trust / TrustedPublisher).** New `scripts/New-IntuneTrustedCertPolicy.ps1` —
708
- a Custom OMA-URI profile that places a certificate into a Windows machine store via the
709
- `RootCATrustedCertificates` CSP (the policy-based way to suppress the Windows "install device software?"
710
- driver-trust prompt). Guide Appendix N, dossier "Treiber-Zertifikat" row, SKILL.md convention + Pester test.
711
-
712
- ### 0.9.2 - 12.06.2026
713
- - **Reconciled a diverged install copy back into the repo.** Fixed `Invoke-PsadtSystemTest.ps1` crashing at
714
- param binding under the WinPS 5.1 re-exec (`$SkillRoot` default is now fail-safe, so the SYSTEM Install/
715
- Uninstall gate works on a pwsh-7 host). Added per-field copy buttons + a `file://`-safe clipboard to the
716
- HTML dossier (`Report-Template.html`, token set unchanged). Added `scripts/New-MsiPackage.ps1` (reusable
717
- MSI package generator, `$PSScriptRoot`-relative, ASCII-only) + its Pester test.
718
-
719
- ### 0.9.1 - 12.06.2026
720
- - **Applicability/portability drift cleanup** (docs + instructions; no script logic changed). Removed the
721
- phantom `test.maxIterations` / `test.endState` config keys from SKILL.md Phase 6 (the cap is a hard count of
722
- 5 the orchestrator owns); fixed stale "Phase 7.5" -> **Phase 9** in SKILL.md + `app-registration.md`; fixed
723
- the README project tree (report **Phase 8**, added the 3 missing scripts `Invoke-PsadtPreflight`,
724
- `Invoke-IntuneAppAssignment`, `_GraphCommon`).
725
- - **`superpowers` downgraded from hard `REQUIRED` to optional/preferred.** The Researcher/Reviewer roles now
726
- prefer `superpowers:*` if installed and otherwise fall back to the native Agent tool / `/code-review`; the
727
- Requirements list documents it as an optional (recommended) enhancement. The workflow no longer depends on it.
728
-
729
- ### 0.9.0 - 11.06.2026
730
- - **Audit cleanup** (quality / content / applicability+compatibility / tests). Highlights: shared
731
- `scripts/_GraphCommon.ps1` (de-duplicates the 3 Graph scripts) + **28 new Pester tests** for the previously
732
- untested upload/assignment/Entra-app/token scripts (suite now 74); fixed an `Invoke-WithRetry` precedence
733
- bug (retried every error 6x) and PS7-fragile throttling reads; the HTML report no longer shows synthetic
734
- "passed" rows when no pre-flight/SYSTEM-test results are supplied (neutral "not run") and derives the PSADT
735
- version from the installed module; GUID `ValidatePattern` on the MSI codes.
736
- - **Phase numbering unified** across SKILL.md + guide into one integer scheme **0-12**; every Phase/Appendix
737
- cross-reference re-verified. Anti-pattern list trimmed; SYSTEM-test prerequisites, `/beta` drift caveat,
738
- rollback step, logging convention, and config help added. Guide `$adtSession` template `1.0.0`->`0.1` +
739
- author-from-config; intro appendix index -> A-M.
740
-
741
- ### 0.8.1 - 11.06.2026
742
- - **Docs consistency.** Fixed a stale cross-reference in SKILL.md (the guide range said **Appendix A-J** but
743
- the guide now runs through **M** — K/L were added in 0.7.0 and M in 0.8.0 without updating it). Restored the
744
- **0.5.3** entry that was missing from this README changelog mirror (it was present in `CHANGELOG.md`).
745
-
746
- ### 0.8.0 - 11.06.2026
747
- - **Opt-in Entra group assignment, wired end-to-end.** New Phase 10 + `Invoke-IntuneAppAssignment.ps1`:
748
- create/reuse Entra security groups by a configured naming scheme (`intune.groups`) and assign the uploaded
749
- app (Required / Available / Uninstall). Read-only dry-run → confirm → execute; idempotent; never deletes a
750
- group or another app's assignment; ambiguous/duplicate names skipped. Least-privilege roles
751
- (`Group.Create` + `GroupMember.Read.All`) via `New-PsadtEntraApp.ps1 -IncludeGroupManagement`. Full
752
- reference in **guide Appendix M** (config schema, naming tokens, version-independent default vs `%version%`
753
- opt-in, permission model).
754
- - **Upload min-OS fix.** `-MinWindowsRelease` is now a `ValidateSet` of backend-accepted release IDs
755
- (`1607..2004`) — `21H2`/`22H2` are server-rejected and used to kill the upload mid-flight with a Graph
756
- `BadRequest`. Fails fast at param binding instead; set a higher minimum in the portal. Guide **H.11**.
757
-
758
- ### 0.7.5 - 10.06.2026
759
- - **Honest exit codes + detection (correctness fix).** Removed the dangerous "always `exit 0`" guidance from
760
- guide Appendix K — a blanket `exit 0` (or a detection tag written in a `finally`) reports GREEN on failure.
761
- New **K.7**: the exit code reflects whether the fix could RUN (couldn't-run -> non-zero; the 64-bit relaunch
762
- propagates the child's exit code), detection reflects the real END-STATE (tag only on success), with a
763
- per-package decision table; "never block enrollment" is now an explicit ESP-assignment + return-code-mapping
764
- choice, not a masked exit code. SKILL.md anti-pattern added.
765
-
766
- ### 0.7.0 - 10.06.2026
767
- - **Value-adding extensions.** New `scripts/Invoke-PsadtPreflight.ps1` turns the Phase-5 Reviewer gate into one
768
- deterministic `GREEN/RED` tool (encoding/parse/v3-scan/top-level/structure/GUID-to-`-FilePath`), with a Pester
769
- suite. New guide **Appendix K** (script-only remediation / fix packages, ESP-safe — the debloat/Cisco pattern)
770
- and **Appendix L** (installer technologies + silent switches, a lookup consulted before web research). Expanded
771
- error-code catalogue (MSI 1603/1605/1619/1638/1639…, PSADT 60001/60008 + ranges) in guide Appendix A and the
772
- SKILL.md troubleshooting table. SKILL.md rewired to point at the pre-flight script and the new appendices.
773
-
774
- ### 0.6.2 - 10.06.2026
775
- - **Audit & harden.** Agent-based audit + source-level verification (discarded ~8 false positives). Fixes: the
776
- guide's broken `New-ADTTemplate` "Extended scaffold" (passing app metadata params that v4.1.x rejects);
777
- the Graph uploader leaving its extracted work dir (with the AES keys in `Detection.xml`) in `%TEMP%` (now
778
- `try/finally` cleanup); the report `Notes` default `&middot;` double-escape; the fallback initials-SVG logo
779
- now XML-escapes + base64-encodes (no markup injection, with a regression test). Robustness: `Invoke-Graph`
780
- 429/5xx retry with `Retry-After`; malformed-`config.json` safety in four scripts; WinGet 2-byte header read;
781
- symmetric temp cleanup in self-update. Graph request shapes left untouched.
782
-
783
- ### 0.6.1 - 10.06.2026
784
- - **Report header flicker fixed** (`references/Report-Template.html`): at widths where the content height met
785
- the viewport edge, the vertical scrollbar toggled on/off and the header's `vw`-based `clamp()` padding and
786
- `h1` font-size reflowed on every toggle — a wild flicker loop. Reserving the scrollbar gutter
787
- (`html { overflow-y: scroll; scrollbar-gutter: stable; }`) keeps the width constant and breaks the loop.
788
-
789
- ### 0.6.0 - 10.06.2026
790
- - **SKILL.md slimmed to a control plane** (733 → 244 lines, ~67% fewer tokens) via progressive disclosure:
791
- the long inline code moved into the reference guide (new **Appendix I** WinGet + **Appendix J** app-logo),
792
- loaded on demand. Intake restructured into **4 decision gates** (researchable facts become stated
793
- assumptions, not questions); explicit **sub-agent roles** (Researcher×3 / Builder / Reviewer) with GREEN
794
- handoff gates; a single **blockade protocol** for errors. No binding rule or behaviour dropped.
795
-
796
- ### 0.5.3 - 09.06.2026
797
- - **Guide code-fences are now English/ASCII.** Anglicized every German comment, string literal and placeholder
798
- living **inside** PowerShell/text code fences in the guide (and the inline-code placeholders in the Appendix
799
- F.1 table) — snippets get copied verbatim into deployment scripts, where the binding rule is English + 7-bit
800
- ASCII. German explanatory **prose** and the **F.2 Company-Portal dossier template** deliberately stay German
801
- (legitimate `language.dossier` end-user text). No script/tooling code changed.
802
-
803
- ### 0.5.2 - 08.06.2026
804
- - **HTML package report is now always generated** (upload or not) by `scripts/New-PsadtReport.ps1` from the
805
- fixed template `references/Report-Template.html`. One self-contained, **bilingual (DE/EN toggle)** document
806
- combining the Intune dossier + a technical package report; Fluent-2 styled, sticky shrink header, logo
807
- embedded as a data URI, description preview rendered from its Markdown source. New Pester test
808
- `tests/New-PsadtReport.Tests.ps1`.
809
-
810
- ### 0.5.1 - 06.06.2026
811
- - Self-update now decides by **commit** (git `HEAD` vs `origin/main`, or the GitHub commits-API sha vs a
812
- recorded `tooling.skillCommit`) instead of the CHANGELOG version — no more CDN lag / circular version reads.
813
- - README project-structure tree compacted so it renders without horizontal scroll.
814
-
815
- ### 0.5.0 - 06.06.2026
816
- - **Skill self-update** — `scripts/Update-PsadtSkill.ps1` checks GitHub for a newer version, shows what's new,
817
- and updates in place on confirmation (`git pull` for a clone, else branch-zip overwrite of tracked files
818
- only; `config.json` / `secret.dpapi` / `tools/` preserved). Triggers: *"update skill"*, *"/update-skill"*,
819
- *"psadt update"*.
820
-
821
- ### 0.4.0 - 06.06.2026
822
- - **WinGet packaging support** (strictly opt-in, never the default) + **certificate-based auth** for the Phase 9
823
- upload (no secret at rest) + MSI icon-table logo fallback + device-code first-poll fix. Contributed by
824
- **@joakim-i** (PR #4), reviewed and hardened before merge. See [CHANGELOG.md](CHANGELOG.md).
825
-
826
- ### 0.3.2 - 06.06.2026
827
- - **Test-before-upload is now a binding gate:** Install + Uninstall must pass the Phase 6 SYSTEM test before
828
- any Phase 9 upload. If it can't be run (no elevation / VM), stop before upload and hand back the command.
829
-
830
- ### 0.3.1 - 06.06.2026
831
- - `Invoke-IntuneWin32Upload.ps1` gains **`-DetectionScriptPath`** (PowerShell-script detection rule) for
832
- EXE / non-MSI installers without a ProductCode (e.g. Vivaldi). Verified live by uploading Vivaldi 8.0.4033.44.
833
- - Lesson: a *detection* script rule accepts only `ruleType,enforceSignatureCheck,runAs32Bit,scriptContent`
834
- (guide Appendix H.2).
835
-
836
- ### 0.3.0 - 06.06.2026
837
- - **Direct upload** (`scripts/Invoke-IntuneWin32Upload.ps1`, Phase 9): self-contained raw-Graph
838
- `win32LobApp` upload (parse `.intunewin` → token → probe → idempotency → create/update → content → SAS
839
- block-blob upload via HttpClient → commit → activate → categories → supersedence). Read-only dry-run by
840
- default; `-Execute` to write.
841
- - **WAM Entra-app bootstrap** (`scripts/New-PsadtEntraApp.ps1`): interactive Windows-broker sign-in (device
842
- code fallback), creates the app + admin consent + secret, DPAPI-stored.
843
- - **App-only token helper** (`scripts/Get-GraphToken.ps1`).
844
- - **Coexistence-safe versioning:** never deletes an older version; new versions coexist; optional
845
- supersedence wiring. **Logo guard:** refuses the PSADT default `AppIcon.png`. Fills the full
846
- App-information tab; never auto-assigns category/notes/groups.
847
- - Fixed the Repair `-FilePath`→`-ProductCode` example; reference guide gains **Appendix H**.
848
-
849
- ### 0.2.0 - 05.06.2026
850
- - **Automated SYSTEM test loop** (`scripts/Invoke-PsadtSystemTest.ps1`, Phase 6): install → uninstall →
851
- reinstall the package as the SYSTEM account via `Invoke-CommandAs`, with agent-driven auto-fix until
852
- green or a max-iteration cap. Opt-in; elevated session required.
853
- - Phase 8 now prefers `Invoke-CommandAs -AsSystem` for SYSTEM-context testing (PsExec kept as a fallback).
854
-
855
- ### 0.1.0 - 04.06.2026
856
- - Initial release: guided PSADT v4 → Intune Win32 lifecycle (intake, autonomous research, scaffolding, all
857
- three deployment types, pre-flight checks, packaging, dossier + logo, guided testing, troubleshooting).
858
- - First-run setup writing a machine-local `config.json` (paths, language, author).
859
- - Self-healing prerequisites: PSAppDeployToolkit module (PSGallery) and `IntuneWinAppUtil.exe`
860
- (auto-download + version check).
861
- - HTML dossier document with a Markdown app-description block (the Intune description field is
862
- Markdown-only).
863
- - English skill + reference guide; MIT licensed.
864
-
865
- </details>
1
+ <h1 align="center">PSADT v4 → Intune Deployment Skill</h1>
2
+
3
+ <p align="center">
4
+ <em>A Claude Code skill that drives the full lifecycle of a PowerShell App Deployment Toolkit (PSADT) v4.x Intune Win32 package — from first conversation to a tested, upload-ready <code>.intunewin</code>.</em>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/pt1987/claude-code-psadt-skill/actions/workflows/tests.yml"><img src="https://github.com/pt1987/claude-code-psadt-skill/actions/workflows/tests.yml/badge.svg" alt="tests" /></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" /></a>
10
+ <img src="https://img.shields.io/badge/PSADT-v4.x-0a7bbb?style=flat-square" alt="PSADT v4.x" />
11
+ <img src="https://img.shields.io/badge/Platform-Windows-0078d6?style=flat-square&logo=windows&logoColor=white" alt="Windows" />
12
+ <img src="https://img.shields.io/badge/Claude%20Code-Skill-d97757?style=flat-square" alt="Claude Code Skill" />
13
+ </p>
14
+
15
+ <p align="center"><sub><a href="#quick-start">Quick start</a> · <a href="#how-it-works">How it works</a> · <a href="#features">Features</a> · <a href="#first-run-setup">Setup</a> · <a href="#security">Security</a> · <a href="#roadmap">Roadmap</a> · <a href="#changelog">Changelog</a></sub></p>
16
+
17
+ ---
18
+
19
+ ## What is this?
20
+
21
+ A **Claude Code skill** (not a plugin): a reusable instruction package that teaches the agent how to build,
22
+ package, test, troubleshoot and deploy a **PSADT v4.x Intune Win32 app**. You describe the application; the
23
+ skill runs the workflow — intake, web research, scaffolding, all three deployment types
24
+ (Install / Uninstall / Repair), pre-flight checks, the SYSTEM test, packaging, the dossier, and the
25
+ optional Graph upload.
26
+
27
+ A skill is a folder with a `SKILL.md` (YAML frontmatter + Markdown instructions), here bundled with
28
+ `scripts/` and `references/`. It loads progressively: the agent sees only the name and description until a
29
+ task makes it relevant, then the full body loads on demand.
30
+
31
+ <img width="1024" height="254" alt="image" src="https://github.com/user-attachments/assets/7c7931ba-dcae-4476-a648-11115eceb3b5" />
32
+
33
+ ## Quick start
34
+
35
+ ```powershell
36
+ npx psadt-deploy-skill
37
+ ```
38
+
39
+ That installs the skill into `~/.claude/skills/psadt-deploy` and runs the setup doctor, which provisions
40
+ everything it can and names the handful of values only you can supply (see
41
+ [First-run setup](#first-run-setup)). Then open Claude Code in any folder and say what you want:
42
+
43
+ > *"Create the Win32 Intune package for 7-Zip 24.09"* — or *"package Notepad++ for Intune"*
44
+
45
+ The skill asks at most **four decision gates** (scope · deployment semantics · SYSTEM-test consent ·
46
+ upload confirmation). Everything else it researches and states as an assumption instead of asking.
47
+
48
+ ## How it works
49
+
50
+ Twelve phases, each owned by a script rather than by prose, so a step either passed or did not:
51
+
52
+ | Phase | What happens | Owner |
53
+ |---|---|---|
54
+ | **0** Setup | 13 prerequisite checks, GREEN/YELLOW/RED, `-Fix` provisions | `Initialize-PsadtSkill.ps1` |
55
+ | **1–2** Intake + research | blocker questions as clickable options; parallel research of version, silent switches, Intune pitfalls | agent (gates 1–2) |
56
+ | **3** Scaffold | a generator writes launcher + detection + per-run log name + manifest; `New-ADTTemplate` only when none fits | `New-MsiPackage` · `New-BrowserExtensionPackage` · `New-WindowsFeaturePackage` · `New-DriverPackage` |
57
+ | **4** Customize | all three hooks filled from the research, helpers in the Extensions module | agent |
58
+ | **5** Pre-flight | 10 checks (encoding, AST parse, v3 cmdlets, structure, detection contract, manifest, log name, driver trust …) → GREEN/RED | `Invoke-PsadtPreflight.ps1` |
59
+ | **6** SYSTEM test | installs/uninstalls as **SYSTEM** like the IME does; **binding before any upload** | `Invoke-PsadtSystemTest.ps1` |
60
+ | **7** Package | one command → verified `.intunewin`, named after the app | `Invoke-PsadtPackage.ps1` |
61
+ | **8** Dossier | always, uploaded or not: bilingual self-contained HTML | `New-PsadtReport.ps1` |
62
+ | **9** Upload *(opt-in)* | dry run → confirm → `win32LobApp` via raw Graph | `Invoke-IntuneWin32Upload.ps1` |
63
+ | **10** Assignment *(opt-in)* | create/reuse Entra groups by naming scheme | `Invoke-IntuneAppAssignment.ps1` |
64
+ | **11–12** Test + rollout | DEV-VM cycles, test group, pilot → staged production | agent |
65
+
66
+ **Everything one app knows lives in `<pkg>\psadt-package.json`** — identity, the decisions taken at the
67
+ gates, the research findings, every phase's result and the artifacts produced. The generators write it,
68
+ every later phase reads and updates it, and pre-flight fails without it. That is what stops two packages of
69
+ the same app from disagreeing about their own version.
70
+
71
+ Depth lives in `references/` (phases 0–12 + appendices A–Q, one file per domain — see
72
+ `references/README.md`); `SKILL.md` stays the
73
+ control plane.
74
+
75
+ ## Features
76
+
77
+ ### Setup and prerequisites
78
+
79
+ - **Setup doctor** — one idempotent script checks PowerShell 7, Windows PowerShell 5.1, elevation, git,
80
+ PSAppDeployToolkit, the content-prep tool, `Invoke-CommandAs`, Pester, the config, a legacy config, the
81
+ skill tree, a pending update and the Intune credentials. Each line carries a concrete fix; `-Fix` applies
82
+ the ones that need no decision.
83
+ - **Self-healing prerequisites** — installs the PSAppDeployToolkit module from the PowerShell Gallery and
84
+ downloads `IntuneWinAppUtil.exe`, keeping both current against their official sources.
85
+ - **Config outside the skill folder** — `config.json`, `secret.dpapi` and `tools/` live in
86
+ `%LOCALAPPDATA%\psadt-deploy\` (override: `$env:PSADT_DEPLOY_HOME`), so a `git pull`, a re-clone or a
87
+ re-install can no longer take your setup with it. A pre-0.19 config keeps working and is migrated on
88
+ request, never deleted.
89
+ - **One-line install** — `npx psadt-deploy-skill` (Node 18+, zero dependencies) does clone-or-update plus
90
+ the doctor run in one step.
91
+
92
+ ### Build and verify
93
+
94
+ - **Guided intake** — the blocker questions up front as clickable options, pre-filled with researched
95
+ defaults (app, latest version, installer type, package type).
96
+ - **Autonomous research** — checks the installed PSADT version against the latest release *and* whether
97
+ commands changed; researches silent install / uninstall / repair switches and known Intune pitfalls.
98
+ - **All three deployment types from the start** — Install, Uninstall *and* Repair, acid-tested, so
99
+ Company-Portal uninstalls actually work.
100
+ - **Pre-flight gate** — encoding/BOM, AST parse, launcher acid test, v3-cmdlet scan, hook structure,
101
+ detection-script contract, the package manifest, the per-run log name and driver trust. GREEN or RED,
102
+ with the failing file named.
103
+ - **Automated SYSTEM test loop** *(opt-in, binding before upload)* — installs, uninstalls and reinstalls as
104
+ the **SYSTEM** account via `Invoke-CommandAs`, mirroring the Intune Management Extension; reads the fresh
105
+ session log and the detection result, and hands back a structured verdict for the fix-and-retry loop.
106
+ Needs an elevated session; belongs on a VM with a snapshot.
107
+ - **Deterministic packaging** — `<outputRoot>\<Vendor>_<App>_<Version>_<Arch>\<same stem>.intunewin`,
108
+ verified after the fact (`Detection.xml`, `SetupFile`, size, SHA256), with the detection script and the
109
+ real logo beside it. It refuses an output folder inside the package, and never deletes a foreign
110
+ `.intunewin` it finds there.
111
+ - **One PSADT log per run** — `<Vendor>_<App>_<Version>_<Arch>_<Install|Uninstall|Repair>_<timestamp>.log`
112
+ instead of every run of every version appending to one unreadable file.
113
+
114
+ ### Package types
115
+
116
+ The app's **native installer is always the default**. Everything else is opt-in and only on request:
117
+
118
+ - **MSI / EXE** — the ordinary case, via `New-MsiPackage.ps1` or a hand-filled scaffold.
119
+ - **WinGet** — the full `PSAppDeployToolkit.WinGet` lifecycle: the extension module self-heals into the
120
+ package, the Package ID is discovered with `Find-ADTWinGetPackage`, hooks use `*-ADTWinGet*`
121
+ (`-Scope Machine`). Never selected on its own initiative.
122
+ - **Browser extensions** — force-install via the Edge/Chrome/Firefox policy keys (including the Firefox
123
+ `REG_MULTI_SZ` trap), with selective removal on uninstall.
124
+ - **Windows features** — `Enable-WindowsOptionalFeature` and `Add-WindowsCapability`, offline source or a
125
+ temporary WSUS bypass that is restored afterwards, `EnablePending` handled honestly.
126
+ - **Third-party drivers** — the trust situation is classified *before* anything is built: Microsoft-signed
127
+ installs silently, vendor-signed needs the signer certificate owned in exactly one place, and a
128
+ vendor-signed **kernel** driver is refused because `TrustedPublisher` satisfies the PnP prompt but never
129
+ Code Integrity — it would install and then not load. Unsigned is refused outright, with three honest
130
+ options and no testsigning. Staging is per-INF `pnputil`; uninstall resolves `oemNN.inf` by original name
131
+ instead of a remembered index.
132
+ - **Script-only / remediation packages** — ESP-safe patterns for fix packages with no installer at all.
133
+
134
+ ### Deliverables
135
+
136
+ - **HTML dossier — always generated**, uploaded or not. One self-contained file
137
+ (`Intune-Dossier.html`) built from a fixed template, never hand-assembled: the **Intune dossier** (App
138
+ Info, return-code map, detection rule, requirements, assignments, driver trust, and a ready-to-paste
139
+ **Markdown** description for the Company-Portal field) plus a **technical package report** (the three
140
+ hooks, PSADT cmdlets used, pre-flight and SYSTEM-test results, logo and `.intunewin` verification).
141
+ Bilingual with a DE/EN toggle, browser-translatable, logo embedded as a data URI.
142
+ - **Real logo only** — finds and downloads the actual application logo (vendor source or Wikimedia
143
+ Commons), verifies real pixel transparency *and* looks at the image. The PSADT default `AppIcon.png` is
144
+ blocked by hash.
145
+ - **Start Menu only** — creates Start Menu entries and removes stray desktop icons.
146
+
147
+ ### Intune
148
+
149
+ - **Access as state, not as a 403** — `Test-PsadtIntuneAccess.ps1` answers before Phase 9 whether the app
150
+ can upload, assign groups or create policies, and for how long the credential lives. Verified / refused /
151
+ **unknown** are three different answers, and an offline check never overwrites what was verified before.
152
+ - **Direct upload via Microsoft Graph** *(opt-in)* — pushes the `.intunewin` as a `win32LobApp` (app +
153
+ logo), self-contained raw Graph, no third-party module. Identity comes from the manifest, so Intune shows
154
+ the same name and version as the artifact and the dossier. Read-only dry run → confirm → upload. Fills the
155
+ whole App-information tab, **never deletes an older version** (new versions coexist, with optional
156
+ supersedence wiring), never auto-assigns categories or notes.
157
+ - **One-time Entra bootstrap** — `New-PsadtEntraApp.ps1` signs in via **WAM**, creates the app, grants and
158
+ admin-consents the roles and stores the credential: a **certificate** (preferred — nothing secret at rest,
159
+ JWT client-assertion auth) or a DPAPI-encrypted client secret. Re-running it is normal: it finds the
160
+ recorded app, merges requested permissions instead of replacing them, and never prompts.
161
+ - **Opt-in group assignment** — creates/reuses Entra security groups by a configured naming scheme and
162
+ assigns Required / Available / Uninstall. Least-privilege (`Group.Create` + `GroupMember.Read.All`),
163
+ dry run → confirm, idempotent, and it never deletes a group or another app's assignment.
164
+ - **Certificate + firewall policies** — Custom OMA-URI profiles for `TrustedPublisher` / `TrustedPeople`
165
+ (the built-in template cannot reach those stores) and settings-catalog firewall-rule policies. Both
166
+ scripts are self-contained deliverables: they can be copied to a test client that has no skill installed.
167
+
168
+ ### Operations
169
+
170
+ - **Troubleshooting** — decodes Intune error/HRESULT codes, maps symptoms to root causes, and triages the
171
+ right log (`AppWorkload.log`, the PSADT session log, `setupapi.dev.log` for drivers).
172
+ - **Self-update** — `scripts/Update-PsadtSkill.ps1` compares against GitHub, shows what changed, and
173
+ updates in place on your confirmation (`git pull --ff-only` for a clone, otherwise a branch-zip overwrite
174
+ of tracked files only). Machine-local state is never touched. Say *"psadt update"*.
175
+ - **441 Pester tests** over the helper scripts, including drift guards that fail when the docs and the code
176
+ disagree.
177
+
178
+ ## Requirements
179
+
180
+ - Windows with PowerShell 5.1+ / PowerShell 7+
181
+ - For the `npx` installer only: **Node 18+** (the skill itself never needs Node)
182
+ - [PSAppDeployToolkit](https://psappdeploytoolkit.com/) v4.x *(installed/updated automatically from the
183
+ PowerShell Gallery if missing)*
184
+ - [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
185
+ *(provisioned automatically)*
186
+ - For the **SYSTEM test loop**: an **elevated** session; the
187
+ [`Invoke-CommandAs`](https://github.com/mkellerman/Invoke-CommandAs) module is installed automatically
188
+ - For the **direct Intune upload**: an Entra app with the Graph application role
189
+ `DeviceManagementApps.ReadWrite.All` (admin-consented) — created in one run by
190
+ `scripts/New-PsadtEntraApp.ps1` (WAM sign-in as Global Admin / Privileged Role Admin, device-code
191
+ fallback). Check what is actually in place with `scripts/Test-PsadtIntuneAccess.ps1`. Full permission
192
+ matrix and the manual portal route: `references/app-registration.md`.
193
+ - For **Pester tests**: Pester 5+ (`Install-Module Pester -MinimumVersion 5.0 -Scope CurrentUser`)
194
+ - **Optional (recommended): the [superpowers](https://github.com/obra/superpowers) plugin** — if installed,
195
+ the research fan-out and the reviewer gate use it. Not required: without it the skill falls back to the
196
+ native Agent tool and `/code-review`, and nothing in the workflow depends on the plugin.
197
+
198
+ ## Installation
199
+
200
+ ```powershell
201
+ npx psadt-deploy-skill
202
+ ```
203
+
204
+ Installs the **newest release** into `~/.claude/skills/psadt-deploy` and runs the setup doctor. Flags:
205
+ `--dir <path>` · `--project` (into `./.claude/skills`) · `--ref <tag|branch>` · `--no-setup`. Node 18+ and
206
+ Windows; the installer itself has zero dependencies and the package carries only `bin/` — the skill is
207
+ fetched from GitHub at install time.
208
+
209
+ ### Which version you get
210
+
211
+ The default is the newest **release tag**, not `main`. This skill registers an Entra application with
212
+ admin consent and writes to an Intune tenant; installing whatever last landed on `main` is not a
213
+ defensible default for that.
214
+
215
+ ```powershell
216
+ npx psadt-deploy-skill # newest release (default)
217
+ npx psadt-deploy-skill --ref v0.26.7 # pin an exact release
218
+ npx psadt-deploy-skill --ref main # the development branch, deliberately
219
+ ```
220
+
221
+ **For managed environments:** pin a tag, read the diff between it and the next one before moving, then
222
+ lift the pin. Releases are tagged `vX.Y.Z` and match the [Changelog](#changelog); tags exist from
223
+ **v0.24.0** onward — earlier versions predate the current history and cannot be tagged retroactively.
224
+
225
+ Re-running the installer updates an existing installation, and so does saying *"psadt update"* to Claude
226
+ Code. What counts as an update depends on what you installed: on a **pinned release** it is the next
227
+ release tag — unreleased work on `main` is deliberately invisible, because that is what pinning means. On
228
+ a **branch** installation it is the next commit, as before. Either way the update overwrites tracked
229
+ repository files only; `config.json`, `secret.dpapi` and `tools/` are never touched.
230
+
231
+ **Or clone it yourself** — the repo root *is* the skill folder:
232
+
233
+ ```powershell
234
+ git clone https://github.com/pt1987/claude-code-psadt-skill.git "$env:USERPROFILE\.claude\skills\psadt-deploy"
235
+ pwsh "$env:USERPROFILE\.claude\skills\psadt-deploy\scripts\Initialize-PsadtSkill.ps1" -Fix
236
+ ```
237
+
238
+ `npx skills add pt1987/claude-code-psadt-skill` works too, since `SKILL.md` sits in the repository root.
239
+
240
+ No git on the machine? The installer falls back to the GitHub tarball and Windows' own `tar.exe`, so the
241
+ one-liner still works — including with `--ref <tag>`, which is the combination a locked-down machine
242
+ actually needs.
243
+
244
+ The skill activates automatically when you ask Claude Code to build an Intune package, or when you work in
245
+ a folder containing `Invoke-AppDeployToolkit.ps1`.
246
+
247
+ ### What is deliberately not in the skill frontmatter
248
+
249
+ `SKILL.md` declares `name`, `description` and `license`, and nothing else. The omissions are choices, not
250
+ oversights:
251
+
252
+ - **`paths`** would look like the right way to express "activates in a folder containing
253
+ `Invoke-AppDeployToolkit.ps1`". It is the opposite: the field *limits* activation to files matching the
254
+ globs. Setting it would switch the skill off for the most common request there is — packaging an app in
255
+ an empty folder, where `Invoke-AppDeployToolkit.ps1` does not exist yet because Phase 3 is what creates
256
+ it. The folder case is covered by the last sentence of the description instead.
257
+ - **`allowed-tools`** grants tools up front; it does not restrict them. For a skill that installs software
258
+ as SYSTEM and writes to a tenant, being asked per call is the point. See [`SECURITY.md`](SECURITY.md).
259
+ - **`metadata.version`** is ignored by Claude Code, and the version already lives in `CHANGELOG.md`,
260
+ `package.json` (kept in sync by a test) and on the website. A fourth place to forget on release day, for
261
+ no behaviour, is not worth it.
262
+ - **`shell`** only matters for `!` command injection in `SKILL.md`, which this skill does not use — and a
263
+ failing `!` command aborts the *entire* skill invocation, so an `Initialize-PsadtSkill` call wired up that
264
+ way would be a single point of failure for every packaging request.
265
+ - **`context: fork` / `agent`** would isolate the skill in a subagent. It orchestrates its own sub-agents
266
+ and needs the main context to hold the decision gates.
267
+
268
+ ## First-run setup
269
+
270
+ `scripts/Initialize-PsadtSkill.ps1` (also reachable by saying *"psadt setup"* / *"psadt doctor"*) checks
271
+ every prerequisite in one pass and reports **GREEN / YELLOW / RED**. Every line comes with a concrete fix
272
+ hint, and `-Fix` applies the ones that need no decision (module installs, the tool download, the
273
+ `language.*` defaults, `paths.intuneWinAppUtil`, and migrating a pre-0.19 config). It is idempotent — run it
274
+ as often as you like.
275
+
276
+ Only four values genuinely need you; the doctor lists them in `.Missing` and takes them via `-Set`:
277
+
278
+ ```powershell
279
+ pwsh scripts/Initialize-PsadtSkill.ps1 -Fix -Set @{
280
+ 'paths.packageRoot' = 'D:\Pakete'; 'paths.outputRoot' = 'D:\Intune'
281
+ 'author.person' = 'Pat Taubert'; 'author.company' = 'PHAT Consulting'
282
+ }
283
+ ```
284
+
285
+ | Setting | Purpose |
286
+ |---|---|
287
+ | `paths.packageRoot` / `outputRoot` | Where packages are built and where artifacts are written |
288
+ | `paths.intuneWinAppUtil` | Content-prep tool location — filled by `-Fix` |
289
+ | `language.script` / `dossier` | Script language (EN) vs. dossier language (DE for the Company Portal) — filled by `-Fix` |
290
+ | `author.person` / `company` | Stamped into every package's `AppScriptAuthor` |
291
+ | `intune.*` *(optional)* | Direct upload: tenant/client, credential reference, verified roles — written by `New-PsadtEntraApp.ps1` |
292
+ | `intune.groups.*` *(optional)* | Opt-in group assignment (`enabled` / `create` / `membershipType` / `naming`) — guide Appendix M |
293
+
294
+ ### Where the setup is stored
295
+
296
+ `config.json`, `secret.dpapi` and `tools/` live in the **config home** — `%LOCALAPPDATA%\psadt-deploy\`,
297
+ overridable with `$env:PSADT_DEPLOY_HOME` — **not** in the skill folder, so they survive a `git pull`, a
298
+ re-clone and a re-install. They are machine-local and never committed. A `config.json` from a pre-0.19
299
+ install (beside `scripts/`) keeps working read-only; the doctor flags it and `-Fix` migrates it, renaming
300
+ the originals to `*.migrated` rather than deleting anything.
301
+
302
+ > DPAPI is bound to the Windows user profile: a re-installed OS invalidates a stored client secret. The
303
+ > doctor and `Test-PsadtIntuneAccess.ps1` both say so, and the fix is one `New-PsadtEntraApp.ps1` run.
304
+
305
+ ## Project structure
306
+
307
+ ```
308
+ psadt-deploy/
309
+ ├─ SKILL.md · README.md · CHANGELOG.md · SECURITY.md · LICENSE
310
+ ├─ package.json · bin/install.mjs the npx installer (Node 18+, zero dependencies)
311
+ ├─ scripts/
312
+ │ │ setup + config
313
+ │ ├─ Initialize-PsadtSkill.ps1 setup doctor (Phase 0, GREEN/YELLOW/RED, -Fix/-Set)
314
+ │ ├─ Get-PsadtConfig.ps1 config read + config-home resolver
315
+ │ ├─ Set-PsadtConfig.ps1 config write (deep merge, DPAPI secret, -Remove)
316
+ │ ├─ Get-PsadtModule.ps1 PSADT module (self-heal)
317
+ │ ├─ Get-IntuneWinAppUtil.ps1 content-prep tool (self-heal)
318
+ │ ├─ Get-WinGetModule.ps1 WinGet extension (opt-in)
319
+ │ ├─ Update-PsadtSkill.ps1 self-update from GitHub
320
+ │ │ per-package truth
321
+ │ ├─ Get-PsadtPackageManifest.ps1 manifest read (+ the artifact stem)
322
+ │ ├─ Set-PsadtPackageManifest.ps1 manifest write (merge / append)
323
+ │ │ package generators
324
+ │ ├─ New-MsiPackage.ps1 MSI packages
325
+ │ ├─ New-BrowserExtensionPackage.ps1 browser-extension force-install (opt-in)
326
+ │ ├─ New-WindowsFeaturePackage.ps1 optional features / capabilities (opt-in)
327
+ │ ├─ New-DriverPackage.ps1 driver packages, pnputil staging (opt-in)
328
+ │ ├─ Get-DriverSignatureInfo.ps1 driver trust classifier (signed? kernel? deployable?)
329
+ │ │ gates + deliverables
330
+ │ ├─ Invoke-PsadtPreflight.ps1 pre-flight GREEN/RED gate (Phase 5, 10 checks)
331
+ │ ├─ Invoke-PsadtSystemTest.ps1 SYSTEM test (Phase 6)
332
+ │ ├─ Invoke-PsadtPackage.ps1 build the .intunewin (Phase 7, named + verified)
333
+ │ ├─ New-PsadtReport.ps1 HTML dossier (Phase 8, always)
334
+ │ │ intune / graph
335
+ │ ├─ New-PsadtEntraApp.ps1 Entra app bootstrap (WAM)
336
+ │ ├─ Get-GraphToken.ps1 app-only Graph token (cert / DPAPI)
337
+ │ ├─ Test-PsadtIntuneAccess.ps1 access verdict (roles, capabilities, expiry)
338
+ │ ├─ Invoke-IntuneWin32Upload.ps1 direct upload (Phase 9)
339
+ │ ├─ Invoke-IntuneAppAssignment.ps1 group assignment (Phase 10, opt-in)
340
+ │ ├─ New-IntuneTrustedCertPolicy.ps1 Custom OMA-URI cert policy (self-contained)
341
+ │ ├─ New-IntuneFirewallPolicy.ps1 firewall-rule policy (self-contained)
342
+ │ ├─ _GraphCommon.ps1 shared Graph helpers (retry, errors, token roles)
343
+ │ └─ _GraphInteractive.ps1 shared WAM sign-in
344
+ ├─ references/
345
+ │ ├─ README.md the reference map (label -> file)
346
+ │ ├─ phases-0-6.md · phases-7-12.md the twelve phases
347
+ │ ├─ appendix-a-errors.md … -q-drivers.md one file per appendix
348
+ │ ├─ Report-Template.html the fixed dossier template
349
+ │ └─ app-registration.md THE Graph permission matrix + manual portal route
350
+ └─ tests/ Pester suite, 441 tests
351
+ ```
352
+
353
+ Machine-local state lives outside the skill folder:
354
+
355
+ ```
356
+ %LOCALAPPDATA%\psadt-deploy\ ($env:PSADT_DEPLOY_HOME overrides)
357
+ ├─ config.json settings incl. the optional intune.* block
358
+ ├─ secret.dpapi DPAPI client secret (only without cert auth)
359
+ └─ tools/ IntuneWinAppUtil.exe + WinGet module
360
+ ```
361
+
362
+ And per package, next to `Invoke-AppDeployToolkit.ps1`:
363
+
364
+ ```
365
+ psadt-package.json identity · gate decisions · research · results · artifacts
366
+ ```
367
+
368
+ ## Status
369
+
370
+ In active use for the full build → package → test → dossier workflow, with the direct Graph upload
371
+ verified against a live tenant. The helper scripts are covered by 441 Pester tests.
372
+
373
+ One open point, honestly: **the driver `pnputil` exit-code semantics are documented, not verified here.**
374
+ `0` / `259` / `3010` and the two `0xE...` failures come from Microsoft's documentation; confirming them
375
+ against `setupapi.dev.log` on a DEV VM with a real vendor-signed and a real Microsoft-signed driver is
376
+ still open.
377
+
378
+ ## Security
379
+
380
+ This skill installs software as SYSTEM, researches on the open web, and writes to an Intune tenant
381
+ through an Entra app with admin consent. [`SECURITY.md`](SECURITY.md) states that risk surface next to
382
+ the control that already covers each part of it — the dry-run-before-execute rule, the three-valued
383
+ access check, never-delete, role assertion before the first write, certificate before DPAPI secret,
384
+ the config home outside the skill folder, and the self-containment rule for anything that ships to a
385
+ test client. Each control names the file that implements it and the test that enforces it, so a review
386
+ can check the claims rather than take them.
387
+
388
+ Two deliberate non-features are explained there as well: the skill does **not** declare
389
+ `allowed-tools` (that field pre-approves tools, it does not restrict them), and content fetched during
390
+ research is treated as data, never as instructions — see
391
+ [`references/research-trust.md`](references/research-trust.md).
392
+
393
+ ## Roadmap
394
+
395
+ Designed and waiting to be built:
396
+
397
+ - **Sync finished packages to a GitHub repo** — a setup option (`output.target` = `local` / `git` / `both`)
398
+ to push the per-app artifacts (`.intunewin`, dossier, detection, logo) to a Git repo instead of, or in
399
+ addition to, a local folder — versioned and shareable. Will need **Git LFS** for large `.intunewin` files
400
+ (GitHub's 100 MB per-file limit).
401
+
402
+ Have a request? Open an issue.
403
+
404
+ ## Contributing
405
+
406
+ Issues and pull requests are welcome. Keep `SKILL.md`, the references and the docs in **English**. The only
407
+ non-English content is the generated end-user output (the Intune dossier and the Company-Portal app
408
+ description), whose language follows the `language.dossier` config value — **default German**, but
409
+ configurable per machine.
410
+
411
+ Two conventions worth knowing before you send a patch: generated `.ps1` content is **7-bit ASCII** (the
412
+ pre-flight fails on non-ASCII without a BOM), and anything that lands in a package's output folder must be
413
+ **self-contained** — it gets copied to test clients that have no skill installed.
414
+
415
+ ## License
416
+
417
+ [MIT](LICENSE) © Patrick Taubert, PHAT Consulting GmbH
418
+
419
+ ## Acknowledgements
420
+
421
+ - [PSAppDeployToolkit](https://github.com/PSAppDeployToolkit/PSAppDeployToolkit)
422
+ - [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
423
+ - [`Invoke-CommandAs`](https://github.com/mkellerman/Invoke-CommandAs)
424
+ - README structure inspired by [ComposioHQ/awesome-claude-skills](https://github.com/ComposioHQ/awesome-claude-skills)
425
+
426
+ ## Changelog
427
+
428
+ Recent releases below; the complete history is in **[CHANGELOG.md](CHANGELOG.md)** (nothing is ever removed
429
+ from either).
430
+
431
+ ### 0.27.0 - 10.09.2026
432
+ - **Fixed: after auto-compaction, half of SKILL.md was gone.** Claude Code re-attaches only the **first
433
+ 5000 tokens** of an invoked skill after a summary. SKILL.md was ~10 10900, so the cut fell at line 198 — the
434
+ middle of Phase 2. In exactly the sessions long enough to compact, the skill lost Phases 3–12, the whole
435
+ troubleshooting table, every anti-pattern and the reference map. The fix is **ordering, not size**: the
436
+ operating mode, the four gates, the conventions and Phases 0–6 now sit ahead of the cut, and Phase 6 ends
437
+ at byte 17 457 of a 17 500-byte budget — with a test that fails if it ever crosses back.
438
+ - **Fixed: `--ref <tag>` returned HTTP 404 on the tarball route.** The installer built
439
+ `tar.gz/refs/heads/<ref>`, and `refs/heads` only resolves *branches* — so pinning worked with git and failed
440
+ on exactly the machines without it, which are the ones most likely to need a pinned release.
441
+ `Update-PsadtSkill.ps1` had the same latent bug in its archive path.
442
+ - **Fixed: a failed install exited 127 instead of 1**, with a libuv assertion after the error message. A
443
+ mistyped `--ref` printed a correct explanation and then looked like a crash.
444
+ - **Changed: the default install is the newest release tag, not `main`.** This skill registers an Entra app
445
+ with admin consent and writes to a tenant; installing whatever last landed on a branch is not a defensible
446
+ default for that. `--ref main` and `--ref v0.27.0` both remain. The update check now distinguishes a
447
+ release-pinned installation (counts *releases* behind) from one following a branch.
448
+ - **Changed: the 2942-line deployment guide is now nineteen files**, one per domain, with
449
+ `references/README.md` as the map. Section numbering is unchanged, so every "App. L.1" and "Phase 6.2"
450
+ still resolves.
451
+ - **Changed (behaviour): Phase 11 no longer re-runs Phase 6.** Phase 6 answers *does the package work* (the
452
+ gate, in a throwaway Sandbox); Phase 11 answers *does the delivery work* (Intune test group, real device,
453
+ `AppWorkload.log`). A package that passes 6 and fails 11 now tells you something.
454
+ - **Changed: `"update skill"` is gone from the description** — un-namespaced, it made this skill answer for
455
+ every other updatable skill on the machine. `"psadt update"` stays.
456
+ - **Added `SECURITY.md`** — the risk surface next to the control that covers each part of it, each naming the
457
+ file and the test that implement it.
458
+ - **Added: researched content is data, never instructions** (`references/research-trust.md`) — Phase 2
459
+ researches on the open web and the result runs as SYSTEM. The install4j case generalised.
460
+ - **Added three guards and CI**: rule anchors proving no binding rule was lost in the move, a two-directional
461
+ doc cross-reference check that also reads `scripts/`, a context-budget test, and the suite on a clean
462
+ Windows runner for every push. Suite 441 → 474.
463
+ - **Added `evals/`** — 20 trigger and behaviour cases. Not yet run: `claude plugin eval` is in early access.
464
+ ### 0.26.7 - 08.09.2026
465
+ - **Fixed: the control plane did not know MSIX exists.** L.8/L.9 landed in 0.26.5, but Gate 1's package-type list
466
+ never mentioned MSIX — so a `.msix` fell through to "native installer (default)", the one route that is wrong for
467
+ it, and nothing pointed at the new appendix. Gate 1 now carries **MSIX/AppX** with the decision attached: Intune
468
+ takes it **natively as a line-of-business app**, so the default is to use that and *not* build a PSADT package;
469
+ the wrapper is the exception, and L.8 is read first when it is used.
470
+ - **Added `tests/SKILL.Tests.ps1`**, a coherence guard: every appendix SKILL.md points at must exist, and the Gate 1
471
+ decision must actually route MSIX. Research that the router cannot reach is dead weight — now a test failure.
472
+ ### 0.26.6 - 08.09.2026
473
+ - **Fixed: the sandbox host timeout force-killed the viewer, then told you to close the window.** A timed-out run
474
+ was torn down like a finished one. That **orphans `vmmemWindowsSandbox`** — the Hyper-V compute service owns it,
475
+ so the host cannot kill it, and it keeps the work folder locked until the next run throws on it — and it removes
476
+ the only window through which the guest could still be shut down cleanly. The script's own comments name this
477
+ mechanism as the reason the *guest* must shut itself down; the timeout path did it anyway. A timeout now touches
478
+ nothing and explains what to close and which knob to raise.
479
+ - **Fixed: the warning claimed 60 seconds while the code waited 180** — a literal duplicating a parameter default.
480
+ One declared value now feeds both.
481
+ - **Appendix G finding 6 corrected**: "no recovery path" is now scoped to a run killed from *outside* the script.
482
+ ### 0.26.5 - 08.09.2026
483
+ - **L.8, new: MSIX/AppX**, verified against Microsoft Learn and against the live cmdlets. MSIX is a two-step
484
+ model — machine-wide **staging**, then **per-user registration at logon** — and three traps follow from it:
485
+ **`Add-AppxPackage` as SYSTEM registers the app for the SYSTEM account and reports success** (nobody can launch
486
+ it), **`Get-AppxPackage` is the wrong detection cmdlet** (finds nothing right after provisioning, so Intune
487
+ reinstalls forever), and **de-provisioning does not remove the app from existing users** — a complete uninstall
488
+ needs `Remove-AppxProvisionedPackage` *and* `Remove-AppxPackage -AllUsers`. Plus signing: the certificate
489
+ Subject must equal the manifest Publisher (so a vendor MSIX cannot just be re-signed), self-signed certs belong
490
+ in `LocalMachine\TrustedPeople`, and missing timestamping is what makes an expired certificate break installs.
491
+ And the decision that comes first: **usually don't wrap MSIX in PSADT** — Intune takes it natively.
492
+ - **L.9, new: App-V is not end of life.** The client and sequencer are **no longer deprecated** (fixed extended
493
+ support, no end date, no extra cost); only the **server components** end in **April 2026**. Also: `-Global` is
494
+ the device-context switch, and a package in use goes *pending* — a global task applies only after a restart.
495
+ - **The MSIX row in L.2 was misleading and is rewritten**; L.2 gains an App-V row.
496
+ ### 0.26.4 - 08.09.2026
497
+ - **L.7, new: Inno Setup and NSIS**, verified against vendor documentation. `/VERYSILENT` **reboots the
498
+ machine by itself** when a restart is needed — so `/NORESTART` is mandatory, and `/RESTARTEXITCODE=3010`
499
+ turns the fact into the code Intune already understands. And `Uninstall.exe /S` in NSIS **returns before
500
+ the uninstall has finished** (it copies itself to temp and detaches), so any verification afterwards races
501
+ it — `_?=<installdir>` makes it synchronous, must be last, must not be quoted.
502
+ - **Advanced Installer wired into L.1 and L.2**, not just the new L.6 — with a measured fingerprint
503
+ (`AI_*` custom actions, `aicustact.dll`, `OLDPRODUCTS;AI_NEWERPRODUCTFOUND`).
504
+ ### 0.26.3 - 08.09.2026
505
+ - **Appendix L gains three sections**, researched against vendor documentation instead of written from
506
+ memory: **MSP patches** (the full `/p` matrix, why `/i` and `/p` never combine, and that exit 1642 means
507
+ *either* "not installed" *or* "wrong patch revision" — so it must not be treated as a blanket success),
508
+ **WiX Burn bundles** (BundleProviderKey rather than a ProductCode for detection; `/layout` is a Burn
509
+ action but the bootstrapper decides whether it can be narrowed — the ADK refuses, so an offline layout is
510
+ always the whole kit), and **Advanced Installer projects** (the `.aip` CLI, a fresh ProductCode per build
511
+ changing the package identity every time, and the undocumented behaviour that relative paths resolve
512
+ against the .aip's own location — moving a project between drives breaks it silently).
513
+ ### 0.26.2 - 08.09.2026
514
+ - **Fixed: a WHQL driver pack turned the pre-flight red.** `Get-DriverSignatureInfo.ps1` did not strip the
515
+ inline comment that WHQL packs write after `CatalogFile=` (`; for WHQL certified`), so the catalog never
516
+ resolved and validly signed drivers classified as `Unsigned` — 6 of 70 INF in a Dell WinPE set, all of
517
+ whose catalogs were Authenticode-valid and Microsoft-signed. An unquoted `;` now ends the value.
518
+ - **Fixed: `New-MsiPackage.ps1` was the sixth script with the `-File` binder trap.** `-ProcessesToClose
519
+ 'a','b'` arrived as one element, so the scaffold closed no processes at all — silently, while reporting
520
+ success. 0.25.1 had fixed the other five and missed the generator.
521
+ - **Documented: seven findings from a three-package dependency chain** (Appendix G) and four new
522
+ anti-patterns (Appendix B), including `msiexec /a` against bundled payload, the `-Include` trap, 8.3
523
+ short names in path comparisons, and what a service's `%TEMP%` really is on Windows 11.
524
+ ### 0.26.1 - 06.09.2026
525
+ - **Fixed: a syntax error in the template silently killed the whole dossier script.** A `
526
+ ` inside a JS
527
+ string literal became a real newline; JavaScript then discards the entire `<script>` block, so the
528
+ language toggle, all copy buttons and the condensing sticky header died at once — while all 419 tests
529
+ stayed green, because they only assert on rendered HTML. `tests/Report-Template.Tests.ps1` now runs
530
+ `node --check` on the script block plus a node-free quote-balance check, both proven against the
531
+ reintroduced bug.
532
+ - **Changed: every table copies by clicking the row**, replacing the hover icon. Key/value tables give the
533
+ value, return codes the code, assignments the group name, the SYSTEM-test table the whole row. Clicks on
534
+ a row's own controls (the foldable detection script) and text selections are left alone. The Markdown
535
+ description keeps its own copy button.
536
+
537
+ ### 0.26.0 - 06.09.2026
538
+ - **Fixed: the dossier could name a return-code type Intune does not have.** Graph accepts exactly
539
+ `success`, `softReboot`, `hardReboot`, `retry`, `failed` — there is no `ignored`, yet "Ignored" reached a
540
+ real dossier because the report rendered a caller-supplied table without validating anything (`Label` was
541
+ free text, `Cls` was interpolated raw into a `class` attribute). `Label` and `Cls` are now derived from
542
+ `Type` through a closed switch and cannot be supplied at all; an invalid type throws.
543
+ - **Return codes are sorted and copyable.** Sorted by type in Appendix F.4 order, then numerically — the
544
+ order the table is verified against and typed into the portal. Each code cell has a copy icon and the
545
+ section a "copy table" button.
546
+ - **New `Get-PsadtReturnCodes.ps1`: one source of truth for the dossier AND the upload.** Previously two
547
+ independent literals that agreed by coincidence. `Invoke-IntuneWin32Upload.ps1` gained `-ReturnCodes`, so
548
+ installer-specific codes finally reach the app instead of only the document, and a new manifest key
549
+ `research.returnCodes` records them once for both.
550
+ - 419 tests; six new regression guards, each verified to fail on its reintroduced bug.
551
+
552
+ ### 0.25.2 - 06.09.2026
553
+ - **Fixed: a sandbox run left an error dialog on the desktop.** The guest shuts itself down at the end, the
554
+ RDP-style client loses its session and shows a connection-lost box. The host now disposes of the viewer as
555
+ soon as the run reports in. The tempting fix — let the host kill the VM instead of shutting the guest down
556
+ — was measured and is wrong: it orphans the `vmmemWindowsSandbox` worker, which then holds the work folder
557
+ open for minutes and cannot be killed. Guest shuts down, host closes the window, and the wait is on the VM
558
+ worker rather than on the client processes.
559
+
560
+ ### 0.25.1 - 06.09.2026
561
+ - **Fixed: array parameters broke under the `-File` binder.** `pwsh script.ps1 -Intents a,b` passes the
562
+ whole string as ONE element, so `Invoke-IntuneAppAssignment.ps1` rejected a perfectly valid comma-separated
563
+ intent list with a ValidateSet error naming a value nobody typed. It now splits the list itself and still
564
+ rejects a real typo by name. `Invoke-PsadtSandboxTest.ps1 -Paths*` had the same trap with a worse outcome —
565
+ only the first path was asserted and the run still reported GREEN. Six regression tests; suite at 382.
566
+
567
+ ### 0.25.0 - 05.09.2026
568
+ - **New: `Get-PsadtMsiFacts.ps1` reads an MSI in one pass** — identity, signature, SHA256, features with
569
+ component counts, *decoded* upgrade flags, shortcuts, directories, file versions, registry rows and the
570
+ Icon table. For an MSI this probe **is** the research: it is what reveals an auto-updater sitting in its
571
+ own feature (so `ADDLOCAL` replaces post-install cleanup), a `DesktopFeature` you must not install, and
572
+ `MigrateFeatures` on the upgrade row — the flag that means `ADDLOCAL` alone is not enough. Five
573
+ regression guards, each verified to fail on its reintroduced bug.
574
+ - **Fixed: the sandbox test left litter and put the evidence in the wrong place.** It kept one work folder
575
+ per app under the config home forever, and `result.json` plus the PSADT logs lived there with the
576
+ manifest pointing into a profile directory. Evidence now lands in
577
+ `<outputRoot>\<Stem>\SandboxTest\` beside the dossier — not in the package folder, which is what
578
+ IntuneWinAppUtil packs — the logs are recorded in `artifacts.logs[]`, and the scratch is removed.
579
+ `-KeepWorkFolder` keeps it, and a failed run keeps it automatically.
580
+ - **Fixed: the test suite wrote into the real config home.** It now redirects `PSADT_DEPLOY_HOME` to a temp
581
+ directory, with a test that asserts it.
582
+ - **Measured:** PuTTY 0.85 packaged with these lessons applied took **13 min 45 s end to end** — versus 75
583
+ minutes for the Notepad++ run that produced them. The largest single factor: the sandbox test costs zero
584
+ wall-clock time when started as soon as pre-flight goes GREEN, because packaging and the dossier do not
585
+ depend on its verdict.
586
+
587
+ ### 0.24.0 - 05.09.2026
588
+ - **New: `Invoke-PsadtSandboxTest.ps1` runs the whole Phase 6 loop in a throwaway Windows Sandbox** —
589
+ Install, detection, Uninstall, detection, Reinstall, Repair, final Uninstall, every action as
590
+ `NT AUTHORITY\SYSTEM` via a scheduled task. **No elevation on the host, and the host is never touched**,
591
+ so the SYSTEM test stops being the step that gets deferred to "a DEV VM later". Every action starts from
592
+ a machine that has never seen the app, so a pass cannot be an artefact of the previous run. The verdict
593
+ is keyed on the detection script; package-specific facts go in via `-PathsPresentAfterInstall` /
594
+ `-PathsAbsentAfterInstall` / `-PathsAbsentAfterUninstall`. Verified end to end on Notepad++ 8.9.8 x64:
595
+ GREEN, all seven steps, 5 min 58 s.
596
+ - **Why it is a script and not a snippet:** a hand-rolled version hit three bugs that each destroyed a full
597
+ VM run and each present as a timeout or a null-reference minutes after launch — `echo %ERRORLEVEL%>file`
598
+ silently becoming the `0>` stdin redirection (empty file, never a number), file existence read as
599
+ completion, and `[string]$null` still being `$null` in Windows PowerShell 5.1 (an empty detection output
600
+ is the *normal* result, so the harness crashed because the package was clean). All three now have
601
+ regression guards that were each verified to fail on the reintroduced bug.
602
+ - Phase 6 in SKILL.md and the guide now lead with the sandbox route; the per-action route stays for apps a
603
+ VM cannot host. New anti-patterns cover hand-rolled harnesses, debugging through a long job instead of a
604
+ two-second local check, and "speeding up" the test by disabling Defender or dropping Uninstall/Repair.
605
+
606
+ ### 0.23.1 - 04.09.2026
607
+ - **Fixed: two generators were broken since 0.21.0.** `New-BrowserExtensionPackage.ps1` and
608
+ `New-WindowsFeaturePackage.ps1` died at run time — the per-run `LogName` change had wedged a statement
609
+ into the template `Replace(...)` chain, which PowerShell parses as a property assignment with an empty
610
+ name. Valid syntax, so neither the parse check nor the source-inspecting tests could see it; it surfaced
611
+ the first time the whole chain ran against a real package. A guard in all four generator test suites now
612
+ fails on a broken chain, and the guard itself was verified against a broken and a good sample.
613
+
614
+ ### 0.23.0 - 04.09.2026
615
+ - **`npx psadt-deploy-skill`.** One line installs or updates the skill into `~/.claude/skills/psadt-deploy`
616
+ and runs the setup doctor. Flags `--dir`, `--project`, `--ref`, `--no-setup`. Cloning to exactly the right
617
+ path by hand was the first thing a new user could get wrong.
618
+ - **Zero dependencies** (Node 18's `fetch` + the `tar.exe` Windows ships). Three acquisition routes:
619
+ update an existing clone, else `git clone --depth 1`, else the branch tarball — because plenty of managed
620
+ machines have no git. The npm package ships only `bin/`; the skill comes from GitHub at install time, so
621
+ a new skill version needs no republish.
622
+ - **The installer never writes the config itself** — it spawns `Set-PsadtConfig.ps1` and
623
+ `Initialize-PsadtSkill.ps1 -Fix`, because a second config-home implementation in JavaScript would drift
624
+ from `Get-PsadtConfig`. `Update-PsadtSkill.ps1` now tracks `package.json` and `bin/` so the archive
625
+ update route stops dropping them. Suite 307 → 326 tests.
626
+
627
+ ### 0.22.0 - 04.09.2026
628
+ - **Third-party drivers.** New `scripts/Get-DriverSignatureInfo.ps1` classifies a driver folder before
629
+ anything is built — Microsoft-signed / vendor-signed / unsigned — by checking the **catalog** signature
630
+ rather than the `.sys` (a dual-signed `.sys` reports only its primary signature). New
631
+ `scripts/New-DriverPackage.ps1` builds the package: pnputil staging per INF, uninstall that resolves
632
+ `oemNN.inf` by original name instead of a remembered index, `Get-WindowsDriver` detection.
633
+ - **The rule that saves the most time:** a vendor-signed *kernel* driver is RED, not a warning. With Secure
634
+ Boot on, only Microsoft Dev-Portal-signed kernel drivers load — importing the signer certificate removes
635
+ the "install device software?" prompt but does nothing for Code Integrity, so the driver installs and
636
+ then never loads. Unsigned drivers are refused outright, with three honest options and no testsigning.
637
+ - Pre-flight gained a `DriverTrust` check that fires for **any** package shipping an `.inf` (a vendor
638
+ installer staging a driver is the case nobody declares), the dossier has a driver-trust row, and
639
+ `New-IntuneTrustedCertPolicy.ps1` is now fully self-contained like the firewall script.
640
+ - New guide **Appendix Q** with the decision tree, pnputil exit codes and the installer-bundled-driver
641
+ case. Suite 251 → 307 tests.
642
+
643
+ ### 0.21.0 - 04.09.2026
644
+ - **Every package gets a manifest** (`psadt-package.json`): identity, gate decisions, research findings,
645
+ every phase result and the produced artifacts, in one file per app. The generators write it, every phase
646
+ script reads and updates it — so an app's version, name and test evidence stop living in someone's head.
647
+ - **The `.intunewin` is named after the app.** New: `<outputRoot>\<Vendor>_<App>_<Version>_<Arch>\<same
648
+ stem>.intunewin`, produced by the new `scripts/Invoke-PsadtPackage.ps1`. Until now every package came out
649
+ as `Invoke-AppDeployToolkit.intunewin` — that name reached Intune, and concurrent uploads collided in one
650
+ shared temp folder. The new script also verifies the archive before calling it a deliverable and refuses
651
+ an output folder inside the package (which made the tool pack its own previous output).
652
+ - **One PSADT log per run** instead of one ever-growing file:
653
+ `<Vendor>_<App>_<Version>_<Arch>_<Install|Uninstall|Repair>_<timestamp>.log`. PSADT appends to a fixed
654
+ name by default, so by the third attempt a failed install was unreadable.
655
+ - **Pre-flight gained two checks:** a missing or incomplete manifest is RED, a pre-0.21 launcher without a
656
+ per-run log name is a WARN. Report and upload both take their identity from the manifest.
657
+ - Guide: the missing `Phase 6 / 9 / 10` sections now exist, and Appendix E is numbered by phase.
658
+ Suite 173 → 251 tests.
659
+
660
+ ### 0.20.0 - 04.09.2026
661
+ - **Intune access is state, not a 403.** New `scripts/Test-PsadtIntuneAccess.ps1` answers *before* Phase 9
662
+ whether the configured app can actually upload, assign groups or create policies — and for how long the
663
+ credential lives. `TokenOk` and every capability are three-valued: verified / refused / **unknown**,
664
+ because Graph tokens are opaque by contract and "we could not tell" is not "not permitted". An offline run
665
+ never overwrites what was verified earlier.
666
+ - **The scripts assert the role they need before their first write** instead of discovering it from a 403
667
+ mid-upload. Group assignment requires *both* group roles and names the missing half.
668
+ - **`New-PsadtEntraApp.ps1` is re-runnable.** It finds the app by the recorded `clientId`, **merges**
669
+ requested permissions instead of replacing them (a run without `-IncludeConfigurationManagement` used to
670
+ silently revoke that role), never prompts, persists what it learned, and only sets `uploadEnabled` once
671
+ consent is really in place. Older client secrets are counted, never deleted.
672
+ - **Auth failures say what to do:** expired/invalid client secret, unknown app or tenant, Conditional Access
673
+ block — and an undecryptable DPAPI secret now explains that DPAPI is bound to the Windows user profile.
674
+ - `references/app-registration.md` is now the single permission matrix (app roles → capabilities → how to
675
+ grant), referenced from the guide instead of duplicated. Suite 128 → 173 tests.
676
+
677
+ ### 0.19.0 - 04.09.2026
678
+ - **Setup doctor: `scripts/Initialize-PsadtSkill.ps1`.** One idempotent script replaces the Phase 0 prose
679
+ wizard and reports GREEN/YELLOW/RED over 13 prerequisite checks, each with a concrete fix hint. `-Fix`
680
+ installs the modules, downloads the content-prep tool, fills the EN/DE + tool-path defaults and migrates an
681
+ old setup; `-Set @{...}` persists your values first; `-Json` / `-JsonPath` for other tooling. `.Missing`
682
+ lists only the four values a human has to supply (`paths.packageRoot`, `paths.outputRoot`, `author.person`,
683
+ `author.company`) — never a key the doctor could fill itself.
684
+ - **Config, secret and tools moved to a per-user config home** (`%LOCALAPPDATA%\psadt-deploy\`, override
685
+ `$env:PSADT_DEPLOY_HOME`) instead of the skill folder, so a `git pull`, re-clone or re-install no longer
686
+ takes the whole setup with it — and scripts started from an output folder still find their config.
687
+ `Get-PsadtConfig.ps1` is the single resolver and now returns `.Home` / `.DefaultHome` / `.LegacyInUse`. An
688
+ old config beside `scripts/` keeps working read-only until `-Fix` migrates it (originals renamed
689
+ `*.migrated`, nothing deleted).
690
+ - **`Set-PsadtConfig.ps1 -Remove`** deletes dotted keys, so switching credential type can clean up the
691
+ stale one. **Fixed:** `New-PsadtEntraApp.ps1` reported `<skill>\config.json` even when the config lived
692
+ elsewhere. Suite 120 → 128 tests.
693
+
694
+
695
+ <details>
696
+ <summary><strong>Earlier releases (0.18.1 and older)</strong></summary>
697
+
698
+ ### 0.18.1 - 03.09.2026
699
+ - **Upload: `-MaxRunTimeMinutes`.** `Invoke-IntuneWin32Upload.ps1` can now set
700
+ `installExperience.maxRunTimeInMinutes` (1–1440); `0` (default) omits the field and keeps the service default
701
+ of 60 min. Raise it for long-running installs (OS in-place upgrades, large suites) so the IME does not kill them.
702
+
703
+ ### 0.18.0 - 01.07.2026
704
+ - **HanseMerkur corporate design + editorial report redesign.** The dossier/report template is re-themed to the
705
+ HanseMerkur CD (green brand family on a light mint canvas; Metric font stack with Segoe fallback and no
706
+ web-font fetch → no CORS console errors on a local `file://` open) and relaid out as an "editorial
707
+ data-report": flat hairline sections, auto-numbered headings (`01…13`), an at-a-glance KPI band under the hero
708
+ (version · pre-flight · min OS · arch), and a wider 1600px layout. The detection script is folded behind a
709
+ collapsed `<details>` (the rule summary stays visible). German report text now uses real umlauts. Fixed the
710
+ sticky-header flicker (Chrome/Edge scroll-anchoring vs. the condensing hero → `overflow-anchor: none`,
711
+ Playwright-verified) and removed the redundant hero status pill.
712
+
713
+ ### 0.17.0 - 01.07.2026
714
+ - **install4j fingerprint + behavioral silent-switch verification.** Appendix L.1 now recognises install4j
715
+ (Java) installers (`com/install4j/runtime`, `exe4j`, `i4jparams.conf`, bundled `jre\`) and records that `/S`
716
+ is NOT its switch (it hangs on the language dialog) — the unattended switch is `-q`, run elevated. New BINDING
717
+ rule: a single string match is a hint, not proof; confirm the engine by its definitive fingerprint AND run the
718
+ silent switch once (timeout+kill, expect exit 0, no dialog) before packaging. Appendix L.3 adds the
719
+ trademark-sign gotcha (`Name(R)` breaks `-match 'Name'` → tolerant regex); Appendix B adds anti-patterns 13–15.
720
+ (Driven by an Aperio install4j installer misidentified as NSIS, where `/S` hung on the language dialog.)
721
+
722
+ ### 0.16.0 - 29.06.2026
723
+ - **Dossier auto-sync convention (BINDING)** + report header layout fix. Any change to the package scripts
724
+ (launcher, Extensions, detection, version/changelog, return codes, re-packaging) now requires regenerating
725
+ `Intune-Dossier.html` in the same pass; a stale dossier is a defect. The `.pill-lg` status badge caps at 230px
726
+ and wraps so a long status no longer overlaps the hero title.
727
+
728
+ ### 0.15.2 - 15.06.2026
729
+ - **Follow-up doc fix.** A contradiction sweep after 0.15.1 caught one more stale "Phase 7.5" in
730
+ `New-PsadtReport.ps1` help (upload is Phase 9); corrected. No other live stale references remain.
731
+
732
+ ### 0.15.1 - 15.06.2026
733
+ - **Generator hardening from a self-review (correctness + security).** All three generators now single-quote-escape
734
+ values embedded in `$adtSession` literals, so an apostrophe in the App name/vendor/author (e.g. "Bob's App",
735
+ "L'Oreal") no longer produces an unparseable package; the MSI `-AdditionalArgumentList` / `ProcessesToClose`
736
+ literals are escaped too (also closing a SYSTEM code-injection path). Detection exit-code drift fixed:
737
+ `New-MsiPackage.ps1` + the WinGet example now `exit 0` for "not installed" (a non-zero exit reads as a detection
738
+ error), and a new **pre-flight Detection check** WARNs on a non-zero exit in `Detect*.ps1`. The WSUS bypass in
739
+ `New-WindowsFeaturePackage.ps1` now saves all prior state before writing and runs inside the `try/finally`, so a
740
+ partial failure can't leave `UseWUServer=0` permanently. Added input guards (`$Name` path-traversal, `__TOKEN__`
741
+ leak), MSI `-Author` config fallback + `-InstallerPath` validation. Stale refs fixed (SKILL.md "A-M"->"A-P",
742
+ `New-PsadtEntraApp.ps1` "Phase 7.5"->"Phase 9").
743
+
744
+ ### 0.15.0 - 15.06.2026
745
+ - **Windows-feature packages (optional features + capabilities / FoD).** New `scripts/New-WindowsFeaturePackage.ps1`
746
+ — one-call generator that enables Windows **Optional Features** (`Enable-WindowsOptionalFeature`: NetFx3,
747
+ Hyper-V, WSL, TelnetClient, …) and **Capabilities / Features on Demand** (`Add-WindowsCapability`: RSAT.*,
748
+ OpenSSH, …) from one typed list, multiple per package. Uninstall reverts (disable/remove); Repair re-enables
749
+ (idempotent). Reboot surfaces **3010** via `$adtSession.SetExitCode(3010)` (`-NoRestart`); detection treats
750
+ `EnablePending` as not-yet-done. Content comes from a bundled `-Source` (offline SxS) else Windows Update
751
+ behind a **temporary** WSUS bypass (`RepairContentServerSource=2`, `UseWUServer=0`) whose exact prior state is
752
+ restored. Guide **Appendix P**, SKILL.md Gate-1/anti-patterns/ref-lookup. Pre-flight GREEN; helper logic
753
+ verified against an in-memory registry sim; ASCII-clean.
754
+
755
+ ### 0.14.0 - 15.06.2026
756
+ - **Browser-extension force-install packages (Edge / Chrome / Firefox).** New
757
+ `scripts/New-BrowserExtensionPackage.ps1` — one-call generator for force-installing browser extensions via
758
+ enterprise **policy registry keys** (policy-only, no installer, ESP-safe). Multiple extensions per package.
759
+ Chromium helper computes the **next free `ExtensionInstallForcelist` index** (never hard-codes `1`), dedupes by
760
+ ID and removes only its own entry (coexistence); Firefox merges into the single `ExtensionSettings` JSON written
761
+ as **`REG_MULTI_SZ`** (single-line `REG_SZ` is silently ignored — Mozilla bug 1750233). Guide **Appendix O**,
762
+ SKILL.md Gate-1/anti-patterns/ref-lookup. Pre-flight GREEN; helpers validated against a scratch registry hive;
763
+ ASCII-clean.
764
+
765
+ ### 0.13.1 - 15.06.2026
766
+ - **Firewall policy body fixed against the live template (verified 201).** `New-IntuneFirewallPolicy.ps1`
767
+ produced a body Graph rejected (400). Corrected via the **msgraph skill** (not guessed): the group id needs
768
+ the `{firewallrulename}` token, the program path is the direct child `..._app_filepath`, action values are
769
+ `_action_type_1`/`_0`, and a template-based policy requires `settingInstanceTemplateReference` per instance +
770
+ `settingValueTemplateReference` per simple/choice value (profiles collection: instance ref only — a per-value
771
+ ref is a duplicate). Confirmed by a live **201 Create**; tests assert the references. Mirrored into the
772
+ MxManagementCenter Output deliverable.
773
+
774
+ ### 0.13.0 - 15.06.2026
775
+ - **Self-contained firewall deliverable (copy-to-client safe).** `scripts/New-IntuneFirewallPolicy.ps1` is now
776
+ fully self-contained — no dot-sourcing of `_GraphCommon`/`_GraphInteractive`, no skill path; WAM sign-in +
777
+ policy body builder + console helpers are embedded. It runs on a test client that does **not** have the skill
778
+ installed (`-Interactive` WAM, or `-GraphToken`). Fixes the "Skill script not found … -SkillRoot" failure when
779
+ the deliverable was copied to another machine. New **binding SKILL.md convention "Self-contained deliverables"**
780
+ + a Pester test that enforces it (no dot-source / no skill path / embeds WAM). Test-first per writing-skills.
781
+
782
+ ### 0.12.0 - 15.06.2026
783
+ - **Interactive WAM sign-in for the Intune policy scripts.** `New-IntuneFirewallPolicy.ps1` and
784
+ `New-IntuneTrustedCertPolicy.ps1` gain `-Interactive` (+ `-TenantId`): delegated sign-in via **WAM**
785
+ (Windows Web Account Manager) when there is no app registration — **no device code**. The WAM machinery
786
+ (`Initialize-MsalBroker` / `Get-WamToken` / new `Get-InteractiveGraphToken`) was extracted from
787
+ `New-PsadtEntraApp.ps1` into a shared `scripts/_GraphInteractive.ps1` (one implementation, no copy-paste
788
+ drift); the bootstrap now consumes it. The MxManagementCenter firewall deliverable became a thin wrapper
789
+ over the generic script.
790
+
791
+ ### 0.11.0 - 15.06.2026
792
+ - **Intune firewall-rules policy + app config-management permission.** New `scripts/New-IntuneFirewallPolicy.ps1`
793
+ (Endpoint Security "Windows Firewall Rules" policy, one program-scoped rule; dry-run / `-Execute` / manual
794
+ portal fallback) + Pester test. Suppresses the first-run Windows Firewall prompt for apps that listen inbound.
795
+ New `New-PsadtEntraApp.ps1 -IncludeConfigurationManagement` consents `DeviceManagementConfiguration.ReadWrite.All`
796
+ (needed by the firewall and trusted-cert policies for `-Execute`).
797
+
798
+ ### 0.10.0 - 15.06.2026
799
+ - **Certificate store deployment (driver-trust / TrustedPublisher).** New `scripts/New-IntuneTrustedCertPolicy.ps1` —
800
+ a Custom OMA-URI profile that places a certificate into a Windows machine store via the
801
+ `RootCATrustedCertificates` CSP (the policy-based way to suppress the Windows "install device software?"
802
+ driver-trust prompt). Guide Appendix N, dossier "Treiber-Zertifikat" row, SKILL.md convention + Pester test.
803
+
804
+ ### 0.9.2 - 12.06.2026
805
+ - **Reconciled a diverged install copy back into the repo.** Fixed `Invoke-PsadtSystemTest.ps1` crashing at
806
+ param binding under the WinPS 5.1 re-exec (`$SkillRoot` default is now fail-safe, so the SYSTEM Install/
807
+ Uninstall gate works on a pwsh-7 host). Added per-field copy buttons + a `file://`-safe clipboard to the
808
+ HTML dossier (`Report-Template.html`, token set unchanged). Added `scripts/New-MsiPackage.ps1` (reusable
809
+ MSI package generator, `$PSScriptRoot`-relative, ASCII-only) + its Pester test.
810
+
811
+ ### 0.9.1 - 12.06.2026
812
+ - **Applicability/portability drift cleanup** (docs + instructions; no script logic changed). Removed the
813
+ phantom `test.maxIterations` / `test.endState` config keys from SKILL.md Phase 6 (the cap is a hard count of
814
+ 5 the orchestrator owns); fixed stale "Phase 7.5" -> **Phase 9** in SKILL.md + `app-registration.md`; fixed
815
+ the README project tree (report **Phase 8**, added the 3 missing scripts `Invoke-PsadtPreflight`,
816
+ `Invoke-IntuneAppAssignment`, `_GraphCommon`).
817
+ - **`superpowers` downgraded from hard `REQUIRED` to optional/preferred.** The Researcher/Reviewer roles now
818
+ prefer `superpowers:*` if installed and otherwise fall back to the native Agent tool / `/code-review`; the
819
+ Requirements list documents it as an optional (recommended) enhancement. The workflow no longer depends on it.
820
+
821
+ ### 0.9.0 - 11.06.2026
822
+ - **Audit cleanup** (quality / content / applicability+compatibility / tests). Highlights: shared
823
+ `scripts/_GraphCommon.ps1` (de-duplicates the 3 Graph scripts) + **28 new Pester tests** for the previously
824
+ untested upload/assignment/Entra-app/token scripts (suite now 74); fixed an `Invoke-WithRetry` precedence
825
+ bug (retried every error 6x) and PS7-fragile throttling reads; the HTML report no longer shows synthetic
826
+ "passed" rows when no pre-flight/SYSTEM-test results are supplied (neutral "not run") and derives the PSADT
827
+ version from the installed module; GUID `ValidatePattern` on the MSI codes.
828
+ - **Phase numbering unified** across SKILL.md + guide into one integer scheme **0-12**; every Phase/Appendix
829
+ cross-reference re-verified. Anti-pattern list trimmed; SYSTEM-test prerequisites, `/beta` drift caveat,
830
+ rollback step, logging convention, and config help added. Guide `$adtSession` template `1.0.0`->`0.1` +
831
+ author-from-config; intro appendix index -> A-M.
832
+
833
+ ### 0.8.1 - 11.06.2026
834
+ - **Docs consistency.** Fixed a stale cross-reference in SKILL.md (the guide range said **Appendix A-J** but
835
+ the guide now runs through **M** — K/L were added in 0.7.0 and M in 0.8.0 without updating it). Restored the
836
+ **0.5.3** entry that was missing from this README changelog mirror (it was present in `CHANGELOG.md`).
837
+
838
+ ### 0.8.0 - 11.06.2026
839
+ - **Opt-in Entra group assignment, wired end-to-end.** New Phase 10 + `Invoke-IntuneAppAssignment.ps1`:
840
+ create/reuse Entra security groups by a configured naming scheme (`intune.groups`) and assign the uploaded
841
+ app (Required / Available / Uninstall). Read-only dry-run → confirm → execute; idempotent; never deletes a
842
+ group or another app's assignment; ambiguous/duplicate names skipped. Least-privilege roles
843
+ (`Group.Create` + `GroupMember.Read.All`) via `New-PsadtEntraApp.ps1 -IncludeGroupManagement`. Full
844
+ reference in **guide Appendix M** (config schema, naming tokens, version-independent default vs `%version%`
845
+ opt-in, permission model).
846
+ - **Upload min-OS fix.** `-MinWindowsRelease` is now a `ValidateSet` of backend-accepted release IDs
847
+ (`1607..2004`) — `21H2`/`22H2` are server-rejected and used to kill the upload mid-flight with a Graph
848
+ `BadRequest`. Fails fast at param binding instead; set a higher minimum in the portal. Guide **H.11**.
849
+
850
+ ### 0.7.5 - 10.06.2026
851
+ - **Honest exit codes + detection (correctness fix).** Removed the dangerous "always `exit 0`" guidance from
852
+ guide Appendix K — a blanket `exit 0` (or a detection tag written in a `finally`) reports GREEN on failure.
853
+ New **K.7**: the exit code reflects whether the fix could RUN (couldn't-run -> non-zero; the 64-bit relaunch
854
+ propagates the child's exit code), detection reflects the real END-STATE (tag only on success), with a
855
+ per-package decision table; "never block enrollment" is now an explicit ESP-assignment + return-code-mapping
856
+ choice, not a masked exit code. SKILL.md anti-pattern added.
857
+
858
+ ### 0.7.0 - 10.06.2026
859
+ - **Value-adding extensions.** New `scripts/Invoke-PsadtPreflight.ps1` turns the Phase-5 Reviewer gate into one
860
+ deterministic `GREEN/RED` tool (encoding/parse/v3-scan/top-level/structure/GUID-to-`-FilePath`), with a Pester
861
+ suite. New guide **Appendix K** (script-only remediation / fix packages, ESP-safe — the debloat/Cisco pattern)
862
+ and **Appendix L** (installer technologies + silent switches, a lookup consulted before web research). Expanded
863
+ error-code catalogue (MSI 1603/1605/1619/1638/1639…, PSADT 60001/60008 + ranges) in guide Appendix A and the
864
+ SKILL.md troubleshooting table. SKILL.md rewired to point at the pre-flight script and the new appendices.
865
+
866
+ ### 0.6.2 - 10.06.2026
867
+ - **Audit & harden.** Agent-based audit + source-level verification (discarded ~8 false positives). Fixes: the
868
+ guide's broken `New-ADTTemplate` "Extended scaffold" (passing app metadata params that v4.1.x rejects);
869
+ the Graph uploader leaving its extracted work dir (with the AES keys in `Detection.xml`) in `%TEMP%` (now
870
+ `try/finally` cleanup); the report `Notes` default `&middot;` double-escape; the fallback initials-SVG logo
871
+ now XML-escapes + base64-encodes (no markup injection, with a regression test). Robustness: `Invoke-Graph`
872
+ 429/5xx retry with `Retry-After`; malformed-`config.json` safety in four scripts; WinGet 2-byte header read;
873
+ symmetric temp cleanup in self-update. Graph request shapes left untouched.
874
+
875
+ ### 0.6.1 - 10.06.2026
876
+ - **Report header flicker fixed** (`references/Report-Template.html`): at widths where the content height met
877
+ the viewport edge, the vertical scrollbar toggled on/off and the header's `vw`-based `clamp()` padding and
878
+ `h1` font-size reflowed on every toggle — a wild flicker loop. Reserving the scrollbar gutter
879
+ (`html { overflow-y: scroll; scrollbar-gutter: stable; }`) keeps the width constant and breaks the loop.
880
+
881
+ ### 0.6.0 - 10.06.2026
882
+ - **SKILL.md slimmed to a control plane** (733 → 244 lines, ~67% fewer tokens) via progressive disclosure:
883
+ the long inline code moved into the reference guide (new **Appendix I** WinGet + **Appendix J** app-logo),
884
+ loaded on demand. Intake restructured into **4 decision gates** (researchable facts become stated
885
+ assumptions, not questions); explicit **sub-agent roles** (Researcher×3 / Builder / Reviewer) with GREEN
886
+ handoff gates; a single **blockade protocol** for errors. No binding rule or behaviour dropped.
887
+
888
+ ### 0.5.3 - 09.06.2026
889
+ - **Guide code-fences are now English/ASCII.** Anglicized every German comment, string literal and placeholder
890
+ living **inside** PowerShell/text code fences in the guide (and the inline-code placeholders in the Appendix
891
+ F.1 table) — snippets get copied verbatim into deployment scripts, where the binding rule is English + 7-bit
892
+ ASCII. German explanatory **prose** and the **F.2 Company-Portal dossier template** deliberately stay German
893
+ (legitimate `language.dossier` end-user text). No script/tooling code changed.
894
+
895
+ ### 0.5.2 - 08.06.2026
896
+ - **HTML package report is now always generated** (upload or not) by `scripts/New-PsadtReport.ps1` from the
897
+ fixed template `references/Report-Template.html`. One self-contained, **bilingual (DE/EN toggle)** document
898
+ combining the Intune dossier + a technical package report; Fluent-2 styled, sticky shrink header, logo
899
+ embedded as a data URI, description preview rendered from its Markdown source. New Pester test
900
+ `tests/New-PsadtReport.Tests.ps1`.
901
+
902
+ ### 0.5.1 - 06.06.2026
903
+ - Self-update now decides by **commit** (git `HEAD` vs `origin/main`, or the GitHub commits-API sha vs a
904
+ recorded `tooling.skillCommit`) instead of the CHANGELOG version — no more CDN lag / circular version reads.
905
+ - README project-structure tree compacted so it renders without horizontal scroll.
906
+
907
+ ### 0.5.0 - 06.06.2026
908
+ - **Skill self-update** — `scripts/Update-PsadtSkill.ps1` checks GitHub for a newer version, shows what's new,
909
+ and updates in place on confirmation (`git pull` for a clone, else branch-zip overwrite of tracked files
910
+ only; `config.json` / `secret.dpapi` / `tools/` preserved). Triggers: *"update skill"*, *"/update-skill"*,
911
+ *"psadt update"*.
912
+
913
+ ### 0.4.0 - 06.06.2026
914
+ - **WinGet packaging support** (strictly opt-in, never the default) + **certificate-based auth** for the Phase 9
915
+ upload (no secret at rest) + MSI icon-table logo fallback + device-code first-poll fix. Contributed by
916
+ **@joakim-i** (PR #4), reviewed and hardened before merge. See [CHANGELOG.md](CHANGELOG.md).
917
+
918
+ ### 0.3.2 - 06.06.2026
919
+ - **Test-before-upload is now a binding gate:** Install + Uninstall must pass the Phase 6 SYSTEM test before
920
+ any Phase 9 upload. If it can't be run (no elevation / VM), stop before upload and hand back the command.
921
+
922
+ ### 0.3.1 - 06.06.2026
923
+ - `Invoke-IntuneWin32Upload.ps1` gains **`-DetectionScriptPath`** (PowerShell-script detection rule) for
924
+ EXE / non-MSI installers without a ProductCode (e.g. Vivaldi). Verified live by uploading Vivaldi 8.0.4033.44.
925
+ - Lesson: a *detection* script rule accepts only `ruleType,enforceSignatureCheck,runAs32Bit,scriptContent`
926
+ (guide Appendix H.2).
927
+
928
+ ### 0.3.0 - 06.06.2026
929
+ - **Direct upload** (`scripts/Invoke-IntuneWin32Upload.ps1`, Phase 9): self-contained raw-Graph
930
+ `win32LobApp` upload (parse `.intunewin` → token → probe → idempotency → create/update → content → SAS
931
+ block-blob upload via HttpClient → commit → activate → categories → supersedence). Read-only dry-run by
932
+ default; `-Execute` to write.
933
+ - **WAM Entra-app bootstrap** (`scripts/New-PsadtEntraApp.ps1`): interactive Windows-broker sign-in (device
934
+ code fallback), creates the app + admin consent + secret, DPAPI-stored.
935
+ - **App-only token helper** (`scripts/Get-GraphToken.ps1`).
936
+ - **Coexistence-safe versioning:** never deletes an older version; new versions coexist; optional
937
+ supersedence wiring. **Logo guard:** refuses the PSADT default `AppIcon.png`. Fills the full
938
+ App-information tab; never auto-assigns category/notes/groups.
939
+ - Fixed the Repair `-FilePath`→`-ProductCode` example; reference guide gains **Appendix H**.
940
+
941
+ ### 0.2.0 - 05.06.2026
942
+ - **Automated SYSTEM test loop** (`scripts/Invoke-PsadtSystemTest.ps1`, Phase 6): install → uninstall →
943
+ reinstall the package as the SYSTEM account via `Invoke-CommandAs`, with agent-driven auto-fix until
944
+ green or a max-iteration cap. Opt-in; elevated session required.
945
+ - Phase 8 now prefers `Invoke-CommandAs -AsSystem` for SYSTEM-context testing (PsExec kept as a fallback).
946
+
947
+ ### 0.1.0 - 04.06.2026
948
+ - Initial release: guided PSADT v4 → Intune Win32 lifecycle (intake, autonomous research, scaffolding, all
949
+ three deployment types, pre-flight checks, packaging, dossier + logo, guided testing, troubleshooting).
950
+ - First-run setup writing a machine-local `config.json` (paths, language, author).
951
+ - Self-healing prerequisites: PSAppDeployToolkit module (PSGallery) and `IntuneWinAppUtil.exe`
952
+ (auto-download + version check).
953
+ - HTML dossier document with a Markdown app-description block (the Intune description field is
954
+ Markdown-only).
955
+ - English skill + reference guide; MIT licensed.
956
+
957
+ </details>