psadt-deploy-skill 0.34.2 → 0.35.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 (2) hide show
  1. package/README.md +477 -454
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,454 +1,477 @@
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; a local-evidence ladder (installed here? binary here? already written down?) answers what it can, and a research agent is dispatched only per question it leaves open | 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
- - **Switch catalog before the web** — identifies the installer engine from the *binary* (byte signatures in
97
- the PE overlay, resources and section table, not a filename guess) and serves that engine's documented
98
- silent / uninstall / log / no-reboot switches from a catalog that ships with the skill. Offline, and it
99
- reports every stage it checked including the misses. A candidate is still a claim until a run proves it.
100
- - **Autonomous research** — checks the installed PSADT version against the latest release *and* whether
101
- commands changed; researches silent install / uninstall / repair switches and known Intune pitfalls.
102
- - **All three deployment types from the start** — Install, Uninstall *and* Repair, acid-tested, so
103
- Company-Portal uninstalls actually work.
104
- - **Pre-flight gate** — encoding/BOM, AST parse, launcher acid test, v3-cmdlet scan, hook structure,
105
- detection-script contract, the package manifest, the per-run log name and driver trust. GREEN or RED,
106
- with the failing file named.
107
- - **Automated SYSTEM test loop** *(opt-in, binding before upload)* — installs, uninstalls and reinstalls as
108
- the **SYSTEM** account via `Invoke-CommandAs`, mirroring the Intune Management Extension; reads the fresh
109
- session log and the detection result, and hands back a structured verdict for the fix-and-retry loop.
110
- Needs an elevated session; belongs on a VM with a snapshot.
111
- - **Deterministic packaging** — `<outputRoot>\<Vendor>_<App>_<Version>_<Arch>\<same stem>.intunewin`,
112
- verified after the fact (`Detection.xml`, `SetupFile`, size, SHA256), with the detection script and the
113
- real logo beside it. It refuses an output folder inside the package, and never deletes a foreign
114
- `.intunewin` it finds there.
115
- - **One PSADT log per run** — `<Vendor>_<App>_<Version>_<Arch>_<Install|Uninstall|Repair>_<timestamp>.log`
116
- instead of every run of every version appending to one unreadable file.
117
-
118
- ### Package types
119
-
120
- The app's **native installer is always the default**. Everything else is opt-in and only on request:
121
-
122
- - **MSI / EXE** — the ordinary case, via `New-MsiPackage.ps1` or a hand-filled scaffold.
123
- - **WinGet** — the full `PSAppDeployToolkit.WinGet` lifecycle: the extension module self-heals into the
124
- package, the Package ID is discovered with `Find-ADTWinGetPackage`, hooks use `*-ADTWinGet*`
125
- (`-Scope Machine`). Never selected on its own initiative.
126
- - **Browser extensions** — force-install via the Edge/Chrome/Firefox policy keys (including the Firefox
127
- `REG_MULTI_SZ` trap), with selective removal on uninstall.
128
- - **Windows features** — `Enable-WindowsOptionalFeature` and `Add-WindowsCapability`, offline source or a
129
- temporary WSUS bypass that is restored afterwards, `EnablePending` handled honestly.
130
- - **Third-party drivers** — the trust situation is classified *before* anything is built: Microsoft-signed
131
- installs silently, vendor-signed needs the signer certificate owned in exactly one place, and a
132
- vendor-signed **kernel** driver is refused because `TrustedPublisher` satisfies the PnP prompt but never
133
- Code Integrity — it would install and then not load. Unsigned is refused outright, with three honest
134
- options and no testsigning. Staging is per-INF `pnputil`; uninstall resolves `oemNN.inf` by original name
135
- instead of a remembered index.
136
- - **Script-only / remediation packages** — ESP-safe patterns for fix packages with no installer at all.
137
-
138
- ### Deliverables
139
-
140
- - **HTML dossier — always generated**, uploaded or not. One self-contained file
141
- (`Intune-Dossier.html`) built from a fixed template, never hand-assembled: the **Intune dossier** (App
142
- Info, return-code map, detection rule, requirements, assignments, driver trust, and a ready-to-paste
143
- **Markdown** description for the Company-Portal field) plus a **technical package report** (the three
144
- hooks, PSADT cmdlets used, pre-flight and SYSTEM-test results, logo and `.intunewin` verification).
145
- Bilingual with a DE/EN toggle, browser-translatable, logo embedded as a data URI.
146
- - **Real logo only** — finds and downloads the actual application logo (vendor source or Wikimedia
147
- Commons), verifies real pixel transparency *and* looks at the image. The PSADT default `AppIcon.png` is
148
- blocked by hash.
149
- - **Start Menu only** — creates Start Menu entries and removes stray desktop icons.
150
-
151
- ### Intune
152
-
153
- - **Access as state, not as a 403** — `Test-PsadtIntuneAccess.ps1` answers before Phase 9 whether the app
154
- can upload, assign groups or create policies, and for how long the credential lives. Verified / refused /
155
- **unknown** are three different answers, and an offline check never overwrites what was verified before.
156
- - **Direct upload via Microsoft Graph** *(opt-in)* — pushes the `.intunewin` as a `win32LobApp` (app +
157
- logo), self-contained raw Graph, no third-party module. Identity comes from the manifest, so Intune shows
158
- the same name and version as the artifact and the dossier. Read-only dry run → confirm → upload. Fills the
159
- whole App-information tab, **never deletes an older version** (new versions coexist, with optional
160
- supersedence wiring), never auto-assigns categories or notes.
161
- - **One-time Entra bootstrap** — `New-PsadtEntraApp.ps1` signs in via **WAM**, creates the app, grants and
162
- admin-consents the roles and stores the credential: a **certificate** (preferred — nothing secret at rest,
163
- JWT client-assertion auth) or a DPAPI-encrypted client secret. Re-running it is normal: it finds the
164
- recorded app, merges requested permissions instead of replacing them, and never prompts.
165
- - **Opt-in group assignment** — creates/reuses Entra security groups by a configured naming scheme and
166
- assigns Required / Available / Uninstall. Least-privilege (`Group.Create` + `GroupMember.Read.All`),
167
- dry run → confirm, idempotent, and it never deletes a group or another app's assignment.
168
- - **Certificate + firewall policies** — Custom OMA-URI profiles for `TrustedPublisher` / `TrustedPeople`
169
- (the built-in template cannot reach those stores) and settings-catalog firewall-rule policies. Both
170
- scripts are self-contained deliverables: they can be copied to a test client that has no skill installed.
171
-
172
- ### Operations
173
-
174
- - **Troubleshooting** — decodes Intune error/HRESULT codes, maps symptoms to root causes, and triages the
175
- right log (`AppWorkload.log`, the PSADT session log, `setupapi.dev.log` for drivers).
176
- - **Self-update** — `scripts/Update-PsadtSkill.ps1` compares against GitHub, shows what changed, and
177
- updates in place on your confirmation (`git pull --ff-only` for a clone, otherwise a branch-zip overwrite
178
- of tracked files only). Machine-local state is never touched. Say *"psadt update"*.
179
- - **503 Pester tests** over the helper scripts, including drift guards that fail when the docs and the code
180
- disagree.
181
-
182
- ## Requirements
183
-
184
- - Windows with PowerShell 5.1+ / PowerShell 7+
185
- - For the `npx` installer only: **Node 18+** (the skill itself never needs Node)
186
- - [PSAppDeployToolkit](https://psappdeploytoolkit.com/) v4.x *(installed/updated automatically from the
187
- PowerShell Gallery if missing)*
188
- - [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
189
- *(provisioned automatically)*
190
- - For the **SYSTEM test loop**: an **elevated** session; the
191
- [`Invoke-CommandAs`](https://github.com/mkellerman/Invoke-CommandAs) module is installed automatically
192
- - For the **direct Intune upload**: an Entra app with the Graph application role
193
- `DeviceManagementApps.ReadWrite.All` (admin-consented) — created in one run by
194
- `scripts/New-PsadtEntraApp.ps1` (WAM sign-in as Global Admin / Privileged Role Admin, device-code
195
- fallback). Check what is actually in place with `scripts/Test-PsadtIntuneAccess.ps1`. Full permission
196
- matrix and the manual portal route: `references/app-registration.md`.
197
- - For **Pester tests**: Pester 5+ (`Install-Module Pester -MinimumVersion 5.0 -Scope CurrentUser`)
198
- - **Optional (recommended): the [superpowers](https://github.com/obra/superpowers) plugin** — if installed,
199
- the gated research fan-out and the reviewer gate use it. Not required: without it the skill falls back to the
200
- native Agent tool and `/code-review`, and nothing in the workflow depends on the plugin.
201
-
202
- ## Installation
203
-
204
- ```powershell
205
- npx psadt-deploy-skill
206
- ```
207
-
208
- Installs the **newest release** into `~/.claude/skills/psadt-deploy` and runs the setup doctor. Flags:
209
- `--dir <path>` · `--project` (into `./.claude/skills`) · `--ref <tag|branch>` · `--no-setup`. Node 18+ and
210
- Windows; the installer itself has zero dependencies and the package carries only `bin/` — the skill is
211
- fetched from GitHub at install time.
212
-
213
- ### Which version you get
214
-
215
- The default is the newest **release tag**, not `main`. This skill registers an Entra application with
216
- admin consent and writes to an Intune tenant; installing whatever last landed on `main` is not a
217
- defensible default for that.
218
-
219
- ```powershell
220
- npx psadt-deploy-skill # newest release (default)
221
- npx psadt-deploy-skill --ref v0.26.7 # pin an exact release
222
- npx psadt-deploy-skill --ref main # the development branch, deliberately
223
- ```
224
-
225
- **For managed environments:** pin a tag, read the diff between it and the next one before moving, then
226
- lift the pin. Releases are tagged `vX.Y.Z` and match the [Changelog](#changelog); tags exist from
227
- **v0.24.0** onward — earlier versions predate the current history and cannot be tagged retroactively.
228
-
229
- Re-running the installer updates an existing installation, and so does saying *"psadt update"* to Claude
230
- Code. What counts as an update depends on what you installed: on a **pinned release** it is the next
231
- release tag — unreleased work on `main` is deliberately invisible, because that is what pinning means. On
232
- a **branch** installation it is the next commit, as before. Either way the update overwrites tracked
233
- repository files only; `config.json`, `secret.dpapi` and `tools/` are never touched.
234
-
235
- **Or clone it yourself** — the repo root *is* the skill folder:
236
-
237
- ```powershell
238
- git clone https://github.com/pt1987/claude-code-psadt-skill.git "$env:USERPROFILE\.claude\skills\psadt-deploy"
239
- pwsh "$env:USERPROFILE\.claude\skills\psadt-deploy\scripts\Initialize-PsadtSkill.ps1" -Fix
240
- ```
241
-
242
- `npx skills add pt1987/claude-code-psadt-skill` works too, since `SKILL.md` sits in the repository root.
243
-
244
- No git on the machine? The installer falls back to the GitHub tarball and Windows' own `tar.exe`, so the
245
- one-liner still works — including with `--ref <tag>`, which is the combination a locked-down machine
246
- actually needs.
247
-
248
- The skill activates automatically when you ask Claude Code to build an Intune package, or when you work in
249
- a folder containing `Invoke-AppDeployToolkit.ps1`.
250
-
251
- ### What is deliberately not in the skill frontmatter
252
-
253
- `SKILL.md` declares `name`, `description` and `license`, and nothing else. The omissions are choices, not
254
- oversights:
255
-
256
- - **`paths`** would look like the right way to express "activates in a folder containing
257
- `Invoke-AppDeployToolkit.ps1`". It is the opposite: the field *limits* activation to files matching the
258
- globs. Setting it would switch the skill off for the most common request there is — packaging an app in
259
- an empty folder, where `Invoke-AppDeployToolkit.ps1` does not exist yet because Phase 3 is what creates
260
- it. The folder case is covered by the last sentence of the description instead.
261
- - **`allowed-tools`** grants tools up front; it does not restrict them. For a skill that installs software
262
- as SYSTEM and writes to a tenant, being asked per call is the point. See [`SECURITY.md`](SECURITY.md).
263
- - **`metadata.version`** is ignored by Claude Code, and the version already lives in `CHANGELOG.md`,
264
- `package.json` (kept in sync by a test) and on the website. A fourth place to forget on release day, for
265
- no behaviour, is not worth it.
266
- - **`shell`** only matters for `!` command injection in `SKILL.md`, which this skill does not use — and a
267
- failing `!` command aborts the *entire* skill invocation, so an `Initialize-PsadtSkill` call wired up that
268
- way would be a single point of failure for every packaging request.
269
- - **`context: fork` / `agent`** would isolate the skill in a subagent. It orchestrates its own sub-agents
270
- and needs the main context to hold the decision gates.
271
-
272
- ## First-run setup
273
-
274
- `scripts/Initialize-PsadtSkill.ps1` (also reachable by saying *"psadt setup"* / *"psadt doctor"*) checks
275
- every prerequisite in one pass and reports **GREEN / YELLOW / RED**. Every line comes with a concrete fix
276
- hint, and `-Fix` applies the ones that need no decision (module installs, the tool download, the
277
- `language.*` defaults, `paths.intuneWinAppUtil`, and migrating a pre-0.19 config). It is idempotent — run it
278
- as often as you like.
279
-
280
- Only four values genuinely need you; the doctor lists them in `.Missing` and takes them via `-Set`:
281
-
282
- ```powershell
283
- pwsh scripts/Initialize-PsadtSkill.ps1 -Fix -Set @{
284
- 'paths.packageRoot' = 'D:\Pakete'; 'paths.outputRoot' = 'D:\Intune'
285
- 'author.person' = 'Pat Taubert'; 'author.company' = 'PHAT Consulting'
286
- }
287
- ```
288
-
289
- | Setting | Purpose |
290
- |---|---|
291
- | `paths.packageRoot` / `outputRoot` | Where packages are built and where artifacts are written |
292
- | `paths.intuneWinAppUtil` | Content-prep tool location — filled by `-Fix` |
293
- | `language.script` / `dossier` | Script language (EN) vs. dossier language (DE for the Company Portal) — filled by `-Fix` |
294
- | `author.person` / `company` | Stamped into every package's `AppScriptAuthor` |
295
- | `intune.*` *(optional)* | Direct upload: tenant/client, credential reference, verified roles — written by `New-PsadtEntraApp.ps1` |
296
- | `intune.groups.*` *(optional)* | Opt-in group assignment (`enabled` / `create` / `membershipType` / `naming`) — guide Appendix M |
297
-
298
- ### Where the setup is stored
299
-
300
- `config.json`, `secret.dpapi` and `tools/` live in the **config home** — `%LOCALAPPDATA%\psadt-deploy\`,
301
- overridable with `$env:PSADT_DEPLOY_HOME` — **not** in the skill folder, so they survive a `git pull`, a
302
- re-clone and a re-install. They are machine-local and never committed. A `config.json` from a pre-0.19
303
- install (beside `scripts/`) keeps working read-only; the doctor flags it and `-Fix` migrates it, renaming
304
- the originals to `*.migrated` rather than deleting anything.
305
-
306
- > DPAPI is bound to the Windows user profile: a re-installed OS invalidates a stored client secret. The
307
- > doctor and `Test-PsadtIntuneAccess.ps1` both say so, and the fix is one `New-PsadtEntraApp.ps1` run.
308
-
309
- ## Project structure
310
-
311
- ```
312
- psadt-deploy/
313
- ├─ SKILL.md · README.md · CHANGELOG.md · SECURITY.md · LICENSE
314
- ├─ package.json · bin/install.mjs the npx installer (Node 18+, zero dependencies)
315
- ├─ scripts/
316
- │ │ setup + config
317
- │ ├─ Initialize-PsadtSkill.ps1 setup doctor (Phase 0, GREEN/YELLOW/RED, -Fix/-Set)
318
- │ ├─ Get-PsadtConfig.ps1 config read + config-home resolver
319
- │ ├─ Set-PsadtConfig.ps1 config write (deep merge, DPAPI secret, -Remove)
320
- │ ├─ Get-PsadtModule.ps1 PSADT module (self-heal)
321
- │ ├─ Get-IntuneWinAppUtil.ps1 content-prep tool (self-heal)
322
- │ ├─ Get-WinGetModule.ps1 WinGet extension (opt-in)
323
- │ ├─ Update-PsadtSkill.ps1 self-update from GitHub
324
- │ │ per-package truth
325
- │ ├─ Get-PsadtPackageManifest.ps1 manifest read (+ the artifact stem)
326
- │ ├─ Set-PsadtPackageManifest.ps1 manifest write (merge / append)
327
- │ │ package generators
328
- │ ├─ New-MsiPackage.ps1 MSI packages
329
- │ ├─ New-BrowserExtensionPackage.ps1 browser-extension force-install (opt-in)
330
- │ ├─ New-WindowsFeaturePackage.ps1 optional features / capabilities (opt-in)
331
- │ ├─ New-DriverPackage.ps1 driver packages, pnputil staging (opt-in)
332
- │ ├─ Get-DriverSignatureInfo.ps1 driver trust classifier (signed? kernel? deployable?)
333
- │ │ gates + deliverables
334
- │ ├─ Invoke-PsadtPreflight.ps1 pre-flight GREEN/RED gate (Phase 5, 10 checks)
335
- │ ├─ Invoke-PsadtSystemTest.ps1 SYSTEM test (Phase 6)
336
- │ ├─ Invoke-PsadtPackage.ps1 build the .intunewin (Phase 7, named + verified)
337
- │ ├─ New-PsadtReport.ps1 HTML dossier (Phase 8, always)
338
- │ │ intune / graph
339
- │ ├─ New-PsadtEntraApp.ps1 Entra app bootstrap (WAM)
340
- │ ├─ Get-GraphToken.ps1 app-only Graph token (cert / DPAPI)
341
- │ ├─ Test-PsadtIntuneAccess.ps1 access verdict (roles, capabilities, expiry)
342
- │ ├─ Invoke-IntuneWin32Upload.ps1 direct upload (Phase 9)
343
- │ ├─ Invoke-IntuneAppAssignment.ps1 group assignment (Phase 10, opt-in)
344
- │ ├─ New-IntuneTrustedCertPolicy.ps1 Custom OMA-URI cert policy (self-contained)
345
- │ ├─ New-IntuneFirewallPolicy.ps1 firewall-rule policy (self-contained)
346
- │ ├─ _GraphCommon.ps1 shared Graph helpers (retry, errors, token roles)
347
- │ └─ _GraphInteractive.ps1 shared WAM sign-in
348
- ├─ references/
349
- │ ├─ README.md the reference map (label -> file)
350
- │ ├─ phases-0-6.md · phases-7-12.md the twelve phases
351
- │ ├─ appendix-a-errors.md … -q-drivers.md one file per appendix
352
- │ ├─ switch-catalog/ engine defaults + JSON schema (App. L.0)
353
- │ ├─ Report-Template.html the fixed dossier template
354
- │ └─ app-registration.md THE Graph permission matrix + manual portal route
355
- └─ tests/ Pester suite, 552 tests
356
- ```
357
-
358
- Machine-local state lives outside the skill folder:
359
-
360
- ```
361
- %LOCALAPPDATA%\psadt-deploy\ ($env:PSADT_DEPLOY_HOME overrides)
362
- ├─ config.json settings incl. the optional intune.* block
363
- ├─ secret.dpapi DPAPI client secret (only without cert auth)
364
- └─ tools/ IntuneWinAppUtil.exe + WinGet module
365
- ```
366
-
367
- And per package, next to `Invoke-AppDeployToolkit.ps1`:
368
-
369
- ```
370
- psadt-package.json identity · gate decisions · research · results · artifacts
371
- ```
372
-
373
- ## Status
374
-
375
- In active use for the full build → package → test → dossier workflow, with the direct Graph upload
376
- verified against a live tenant. The helper scripts are covered by 503 Pester tests.
377
-
378
- One open point, honestly: **the driver `pnputil` exit-code semantics are documented, not verified here.**
379
- `0` / `259` / `3010` and the two `0xE...` failures come from Microsoft's documentation; confirming them
380
- against `setupapi.dev.log` on a DEV VM with a real vendor-signed and a real Microsoft-signed driver is
381
- still open.
382
-
383
- ## Security
384
-
385
- This skill installs software as SYSTEM, researches on the open web, and writes to an Intune tenant
386
- through an Entra app with admin consent. [`SECURITY.md`](SECURITY.md) states that risk surface next to
387
- the control that already covers each part of it — the dry-run-before-execute rule, the three-valued
388
- access check, never-delete, role assertion before the first write, certificate before DPAPI secret,
389
- the config home outside the skill folder, and the self-containment rule for anything that ships to a
390
- test client. Each control names the file that implements it and the test that enforces it, so a review
391
- can check the claims rather than take them.
392
-
393
- Two deliberate non-features are explained there as well: the skill does **not** declare
394
- `allowed-tools` (that field pre-approves tools, it does not restrict them), and content fetched during
395
- research is treated as data, never as instructions — see
396
- [`references/research-trust.md`](references/research-trust.md).
397
-
398
- ## Roadmap
399
-
400
- Designed and waiting to be built:
401
-
402
- - **Sync finished packages to a GitHub repo** — a setup option (`output.target` = `local` / `git` / `both`)
403
- to push the per-app artifacts (`.intunewin`, dossier, detection, logo) to a Git repo instead of, or in
404
- addition to, a local folder — versioned and shareable. Will need **Git LFS** for large `.intunewin` files
405
- (GitHub's 100 MB per-file limit).
406
-
407
- Have a request? Open an issue.
408
-
409
- ## Contributing
410
-
411
- Issues and pull requests are welcome. Keep `SKILL.md`, the references and the docs in **English**. The only
412
- non-English content is the generated end-user output (the Intune dossier and the Company-Portal app
413
- description), whose language follows the `language.dossier` config value — **default German**, but
414
- configurable per machine.
415
-
416
- Two conventions worth knowing before you send a patch: generated `.ps1` content is **7-bit ASCII** (the
417
- pre-flight fails on non-ASCII without a BOM), and anything that lands in a package's output folder must be
418
- **self-contained** — it gets copied to test clients that have no skill installed.
419
-
420
- ## License
421
-
422
- [MIT](LICENSE) © Patrick Taubert, PHAT Consulting GmbH
423
-
424
- ## Acknowledgements
425
-
426
- - [PSAppDeployToolkit](https://github.com/PSAppDeployToolkit/PSAppDeployToolkit)
427
- - [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
428
- - [`Invoke-CommandAs`](https://github.com/mkellerman/Invoke-CommandAs)
429
- - README structure inspired by [ComposioHQ/awesome-claude-skills](https://github.com/ComposioHQ/awesome-claude-skills)
430
-
431
- ## Changelog
432
-
433
- The two most recent releases are below. **[CHANGELOG.md](CHANGELOG.md)** carries the complete history,
434
- every release since 0.1.0, and nothing is ever removed from it - this section is a window onto it, not a
435
- second copy to keep in sync.
436
-
437
- ### 0.34.2 - 2026-09-16
438
- - **Added: `tests/SiteFigures.Tests.ps1`** - the landing page's stat tiles are now derived from the
439
- repository and compared, instead of being hand-maintained and unchecked. A reader found the page
440
- claiming 19 installer engines in the tile and "1 of 14" in the Phase 2 step right below it; the
441
- script count had also been one behind since 0.33.0. index.html lives on gh-pages, so the suite had
442
- never seen it. The guard reads it out of that ref, skips itself when the ref is absent, and the
443
- workflow fetches it so CI checks it for real. Both stale figures fixed. Suite 624 -> 631.
444
-
445
- ### 0.34.1 - 2026-09-16
446
- - **Changed: SKILL.md now names `-TrustedPublisherCert` at Phase 6**, with `-Scenarios` for iteration and
447
- `STOP.txt` for cancelling. 0.34.0 documented the certificate in phase 6.1 only, because the control
448
- plane had 55 bytes of headroom against its 5000-token budget - the wrong trade for a failure that is
449
- expensive and silent, since an agent that never opens 6.1 repeats the 25-minute timeout blind. Room was
450
- made the way the budget test prescribes: `rule:author-version-changelog` and `rule:start-menu-only`
451
- moved to `references/conventions.md`, which already carried their full text and which SKILL.md routes
452
- to. Both are editorial rules - their failure costs a doc edit or a stray desktop icon, not a
453
- deployment. Control plane 17348 / 17500 bytes, headroom 55 -> 152. Suite 624, unchanged.
454
-
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; a local-evidence ladder (installed here? binary here? already written down?) answers what it can, and a research agent is dispatched only per question it leaves open | 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
+ - **Switch catalog before the web** — identifies the installer engine from the *binary* (byte signatures in
97
+ the PE overlay, resources and section table, not a filename guess) and serves that engine's documented
98
+ silent / uninstall / log / no-reboot switches from a catalog that ships with the skill. Offline, and it
99
+ reports every stage it checked including the misses. A candidate is still a claim until a run proves it.
100
+ - **Autonomous research** — checks the installed PSADT version against the latest release *and* whether
101
+ commands changed; researches silent install / uninstall / repair switches and known Intune pitfalls.
102
+ - **All three deployment types from the start** — Install, Uninstall *and* Repair, acid-tested, so
103
+ Company-Portal uninstalls actually work.
104
+ - **Pre-flight gate** — encoding/BOM, AST parse, launcher acid test, v3-cmdlet scan, hook structure,
105
+ detection-script contract, the package manifest, the per-run log name and driver trust. GREEN or RED,
106
+ with the failing file named.
107
+ - **Automated SYSTEM test loop** *(opt-in, binding before upload)* — installs, uninstalls and reinstalls as
108
+ the **SYSTEM** account via `Invoke-CommandAs`, mirroring the Intune Management Extension; reads the fresh
109
+ session log and the detection result, and hands back a structured verdict for the fix-and-retry loop.
110
+ Needs an elevated session; belongs on a VM with a snapshot.
111
+ - **Deterministic packaging** — `<outputRoot>\<Vendor>_<App>_<Version>_<Arch>\<same stem>.intunewin`,
112
+ verified after the fact (`Detection.xml`, `SetupFile`, size, SHA256), with the detection script and the
113
+ real logo beside it. It refuses an output folder inside the package, and never deletes a foreign
114
+ `.intunewin` it finds there.
115
+ - **One PSADT log per run** — `<Vendor>_<App>_<Version>_<Arch>_<Install|Uninstall|Repair>_<timestamp>.log`
116
+ instead of every run of every version appending to one unreadable file.
117
+
118
+ ### Package types
119
+
120
+ The app's **native installer is always the default**. Everything else is opt-in and only on request:
121
+
122
+ - **MSI / EXE** — the ordinary case, via `New-MsiPackage.ps1` or a hand-filled scaffold.
123
+ - **WinGet** — the full `PSAppDeployToolkit.WinGet` lifecycle: the extension module self-heals into the
124
+ package, the Package ID is discovered with `Find-ADTWinGetPackage`, hooks use `*-ADTWinGet*`
125
+ (`-Scope Machine`). Never selected on its own initiative.
126
+ - **Browser extensions** — force-install via the Edge/Chrome/Firefox policy keys (including the Firefox
127
+ `REG_MULTI_SZ` trap), with selective removal on uninstall.
128
+ - **Windows features** — `Enable-WindowsOptionalFeature` and `Add-WindowsCapability`, offline source or a
129
+ temporary WSUS bypass that is restored afterwards, `EnablePending` handled honestly.
130
+ - **Third-party drivers** — the trust situation is classified *before* anything is built: Microsoft-signed
131
+ installs silently, vendor-signed needs the signer certificate owned in exactly one place, and a
132
+ vendor-signed **kernel** driver is refused because `TrustedPublisher` satisfies the PnP prompt but never
133
+ Code Integrity — it would install and then not load. Unsigned is refused outright, with three honest
134
+ options and no testsigning. Staging is per-INF `pnputil`; uninstall resolves `oemNN.inf` by original name
135
+ instead of a remembered index.
136
+ - **Script-only / remediation packages** — ESP-safe patterns for fix packages with no installer at all.
137
+
138
+ ### Deliverables
139
+
140
+ - **HTML dossier — always generated**, uploaded or not. One self-contained file
141
+ (`Intune-Dossier.html`) built from a fixed template, never hand-assembled: the **Intune dossier** (App
142
+ Info, return-code map, detection rule, requirements, assignments, driver trust, and a ready-to-paste
143
+ **Markdown** description for the Company-Portal field) plus a **technical package report** (the three
144
+ hooks, PSADT cmdlets used, pre-flight and SYSTEM-test results, logo and `.intunewin` verification).
145
+ Bilingual with a DE/EN toggle, browser-translatable, logo embedded as a data URI.
146
+ - **Real logo only** — finds and downloads the actual application logo (vendor source or Wikimedia
147
+ Commons), verifies real pixel transparency *and* looks at the image. The PSADT default `AppIcon.png` is
148
+ blocked by hash.
149
+ - **Start Menu only** — creates Start Menu entries and removes stray desktop icons.
150
+
151
+ ### Intune
152
+
153
+ - **Access as state, not as a 403** — `Test-PsadtIntuneAccess.ps1` answers before Phase 9 whether the app
154
+ can upload, assign groups or create policies, and for how long the credential lives. Verified / refused /
155
+ **unknown** are three different answers, and an offline check never overwrites what was verified before.
156
+ - **Direct upload via Microsoft Graph** *(opt-in)* — pushes the `.intunewin` as a `win32LobApp` (app +
157
+ logo), self-contained raw Graph, no third-party module. Identity comes from the manifest, so Intune shows
158
+ the same name and version as the artifact and the dossier. Read-only dry run → confirm → upload. Fills the
159
+ whole App-information tab, **never deletes an older version** (new versions coexist, with optional
160
+ supersedence wiring), never auto-assigns categories or notes.
161
+ - **One-time Entra bootstrap** — `New-PsadtEntraApp.ps1` signs in via **WAM**, creates the app, grants and
162
+ admin-consents the roles and stores the credential: a **certificate** (preferred — nothing secret at rest,
163
+ JWT client-assertion auth) or a DPAPI-encrypted client secret. Re-running it is normal: it finds the
164
+ recorded app, merges requested permissions instead of replacing them, and never prompts.
165
+ - **Opt-in group assignment** — creates/reuses Entra security groups by a configured naming scheme and
166
+ assigns Required / Available / Uninstall. Least-privilege (`Group.Create` + `GroupMember.Read.All`),
167
+ dry run → confirm, idempotent, and it never deletes a group or another app's assignment.
168
+ - **Certificate + firewall policies** — Custom OMA-URI profiles for `TrustedPublisher` / `TrustedPeople`
169
+ (the built-in template cannot reach those stores) and settings-catalog firewall-rule policies. Both
170
+ scripts are self-contained deliverables: they can be copied to a test client that has no skill installed.
171
+
172
+ ### Operations
173
+
174
+ - **Troubleshooting** — decodes Intune error/HRESULT codes, maps symptoms to root causes, and triages the
175
+ right log (`AppWorkload.log`, the PSADT session log, `setupapi.dev.log` for drivers).
176
+ - **Self-update** — `scripts/Update-PsadtSkill.ps1` compares against GitHub, shows what changed, and
177
+ updates in place on your confirmation (`git pull --ff-only` for a clone, otherwise a branch-zip overwrite
178
+ of tracked files only). Machine-local state is never touched. Say *"psadt update"*.
179
+ - **503 Pester tests** over the helper scripts, including drift guards that fail when the docs and the code
180
+ disagree.
181
+
182
+ ## Requirements
183
+
184
+ - Windows with PowerShell 5.1+ / PowerShell 7+
185
+ - For the `npx` installer only: **Node 18+** (the skill itself never needs Node)
186
+ - [PSAppDeployToolkit](https://psappdeploytoolkit.com/) v4.x *(installed/updated automatically from the
187
+ PowerShell Gallery if missing)*
188
+ - [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
189
+ *(provisioned automatically)*
190
+ - For the **SYSTEM test loop**: an **elevated** session; the
191
+ [`Invoke-CommandAs`](https://github.com/mkellerman/Invoke-CommandAs) module is installed automatically
192
+ - For the **direct Intune upload**: an Entra app with the Graph application role
193
+ `DeviceManagementApps.ReadWrite.All` (admin-consented) — created in one run by
194
+ `scripts/New-PsadtEntraApp.ps1` (WAM sign-in as Global Admin / Privileged Role Admin, device-code
195
+ fallback). Check what is actually in place with `scripts/Test-PsadtIntuneAccess.ps1`. Full permission
196
+ matrix and the manual portal route: `references/app-registration.md`.
197
+ - For **Pester tests**: Pester 5+ (`Install-Module Pester -MinimumVersion 5.0 -Scope CurrentUser`)
198
+ - **Optional (recommended): the [superpowers](https://github.com/obra/superpowers) plugin** — if installed,
199
+ the gated research fan-out and the reviewer gate use it. Not required: without it the skill falls back to the
200
+ native Agent tool and `/code-review`, and nothing in the workflow depends on the plugin.
201
+
202
+ ## Installation
203
+
204
+ ```powershell
205
+ npx psadt-deploy-skill
206
+ ```
207
+
208
+ Installs the **newest release** into `~/.claude/skills/psadt-deploy` and runs the setup doctor. Flags:
209
+ `--dir <path>` · `--project` (into `./.claude/skills`) · `--ref <tag|branch>` · `--no-setup`. Node 18+ and
210
+ Windows; the installer itself has zero dependencies and the package carries only `bin/` — the skill is
211
+ fetched from GitHub at install time.
212
+
213
+ ### Which version you get
214
+
215
+ The default is the newest **release tag**, not `main`. This skill registers an Entra application with
216
+ admin consent and writes to an Intune tenant; installing whatever last landed on `main` is not a
217
+ defensible default for that.
218
+
219
+ ```powershell
220
+ npx psadt-deploy-skill # newest release (default)
221
+ npx psadt-deploy-skill --ref v0.26.7 # pin an exact release
222
+ npx psadt-deploy-skill --ref main # the development branch, deliberately
223
+ ```
224
+
225
+ **For managed environments:** pin a tag, read the diff between it and the next one before moving, then
226
+ lift the pin. Releases are tagged `vX.Y.Z` and match the [Changelog](#changelog); tags exist from
227
+ **v0.24.0** onward — earlier versions predate the current history and cannot be tagged retroactively.
228
+
229
+ Re-running the installer updates an existing installation, and so does saying *"psadt update"* to Claude
230
+ Code. What counts as an update depends on what you installed: on a **pinned release** it is the next
231
+ release tag — unreleased work on `main` is deliberately invisible, because that is what pinning means. On
232
+ a **branch** installation it is the next commit, as before. Either way the update overwrites tracked
233
+ repository files only; `config.json`, `secret.dpapi` and `tools/` are never touched.
234
+
235
+ **Or clone it yourself** — the repo root *is* the skill folder:
236
+
237
+ ```powershell
238
+ git clone https://github.com/pt1987/claude-code-psadt-skill.git "$env:USERPROFILE\.claude\skills\psadt-deploy"
239
+ pwsh "$env:USERPROFILE\.claude\skills\psadt-deploy\scripts\Initialize-PsadtSkill.ps1" -Fix
240
+ ```
241
+
242
+ `npx skills add pt1987/claude-code-psadt-skill` works too, since `SKILL.md` sits in the repository root.
243
+
244
+ No git on the machine? The installer falls back to the GitHub tarball and Windows' own `tar.exe`, so the
245
+ one-liner still works — including with `--ref <tag>`, which is the combination a locked-down machine
246
+ actually needs.
247
+
248
+ The skill activates automatically when you ask Claude Code to build an Intune package, or when you work in
249
+ a folder containing `Invoke-AppDeployToolkit.ps1`.
250
+
251
+ ### What is deliberately not in the skill frontmatter
252
+
253
+ `SKILL.md` declares `name`, `description` and `license`, and nothing else. The omissions are choices, not
254
+ oversights:
255
+
256
+ - **`paths`** would look like the right way to express "activates in a folder containing
257
+ `Invoke-AppDeployToolkit.ps1`". It is the opposite: the field *limits* activation to files matching the
258
+ globs. Setting it would switch the skill off for the most common request there is — packaging an app in
259
+ an empty folder, where `Invoke-AppDeployToolkit.ps1` does not exist yet because Phase 3 is what creates
260
+ it. The folder case is covered by the last sentence of the description instead.
261
+ - **`allowed-tools`** grants tools up front; it does not restrict them. For a skill that installs software
262
+ as SYSTEM and writes to a tenant, being asked per call is the point. See [`SECURITY.md`](SECURITY.md).
263
+ - **`metadata.version`** is ignored by Claude Code, and the version already lives in `CHANGELOG.md`,
264
+ `package.json` (kept in sync by a test) and on the website. A fourth place to forget on release day, for
265
+ no behaviour, is not worth it.
266
+ - **`shell`** only matters for `!` command injection in `SKILL.md`, which this skill does not use — and a
267
+ failing `!` command aborts the *entire* skill invocation, so an `Initialize-PsadtSkill` call wired up that
268
+ way would be a single point of failure for every packaging request.
269
+ - **`context: fork` / `agent`** would isolate the skill in a subagent. It orchestrates its own sub-agents
270
+ and needs the main context to hold the decision gates.
271
+
272
+ ## First-run setup
273
+
274
+ `scripts/Initialize-PsadtSkill.ps1` (also reachable by saying *"psadt setup"* / *"psadt doctor"*) checks
275
+ every prerequisite in one pass and reports **GREEN / YELLOW / RED**. Every line comes with a concrete fix
276
+ hint, and `-Fix` applies the ones that need no decision (module installs, the tool download, the
277
+ `language.*` defaults, `paths.intuneWinAppUtil`, and migrating a pre-0.19 config). It is idempotent — run it
278
+ as often as you like.
279
+
280
+ Only four values genuinely need you; the doctor lists them in `.Missing` and takes them via `-Set`:
281
+
282
+ ```powershell
283
+ pwsh scripts/Initialize-PsadtSkill.ps1 -Fix -Set @{
284
+ 'paths.packageRoot' = 'D:\Pakete'; 'paths.outputRoot' = 'D:\Intune'
285
+ 'author.person' = 'Pat Taubert'; 'author.company' = 'PHAT Consulting'
286
+ }
287
+ ```
288
+
289
+ | Setting | Purpose |
290
+ |---|---|
291
+ | `paths.packageRoot` / `outputRoot` | Where packages are built and where artifacts are written |
292
+ | `paths.intuneWinAppUtil` | Content-prep tool location — filled by `-Fix` |
293
+ | `language.script` / `dossier` | Script language (EN) vs. dossier language (DE for the Company Portal) — filled by `-Fix` |
294
+ | `author.person` / `company` | Stamped into every package's `AppScriptAuthor` |
295
+ | `intune.*` *(optional)* | Direct upload: tenant/client, credential reference, verified roles — written by `New-PsadtEntraApp.ps1` |
296
+ | `intune.groups.*` *(optional)* | Opt-in group assignment (`enabled` / `create` / `membershipType` / `naming`) — guide Appendix M |
297
+
298
+ ### Where the setup is stored
299
+
300
+ `config.json`, `secret.dpapi` and `tools/` live in the **config home** — `%LOCALAPPDATA%\psadt-deploy\`,
301
+ overridable with `$env:PSADT_DEPLOY_HOME` — **not** in the skill folder, so they survive a `git pull`, a
302
+ re-clone and a re-install. They are machine-local and never committed. A `config.json` from a pre-0.19
303
+ install (beside `scripts/`) keeps working read-only; the doctor flags it and `-Fix` migrates it, renaming
304
+ the originals to `*.migrated` rather than deleting anything.
305
+
306
+ > DPAPI is bound to the Windows user profile: a re-installed OS invalidates a stored client secret. The
307
+ > doctor and `Test-PsadtIntuneAccess.ps1` both say so, and the fix is one `New-PsadtEntraApp.ps1` run.
308
+
309
+ ## Project structure
310
+
311
+ ```
312
+ psadt-deploy/
313
+ ├─ SKILL.md · README.md · CHANGELOG.md · SECURITY.md · LICENSE
314
+ ├─ package.json · bin/install.mjs the npx installer (Node 18+, zero dependencies)
315
+ ├─ scripts/
316
+ │ │ setup + config
317
+ │ ├─ Initialize-PsadtSkill.ps1 setup doctor (Phase 0, GREEN/YELLOW/RED, -Fix/-Set)
318
+ │ ├─ Get-PsadtConfig.ps1 config read + config-home resolver
319
+ │ ├─ Set-PsadtConfig.ps1 config write (deep merge, DPAPI secret, -Remove)
320
+ │ ├─ Get-PsadtModule.ps1 PSADT module (self-heal)
321
+ │ ├─ Get-IntuneWinAppUtil.ps1 content-prep tool (self-heal)
322
+ │ ├─ Get-WinGetModule.ps1 WinGet extension (opt-in)
323
+ │ ├─ Update-PsadtSkill.ps1 self-update from GitHub
324
+ │ │ per-package truth
325
+ │ ├─ Get-PsadtPackageManifest.ps1 manifest read (+ the artifact stem)
326
+ │ ├─ Set-PsadtPackageManifest.ps1 manifest write (merge / append)
327
+ │ │ package generators
328
+ │ ├─ New-MsiPackage.ps1 MSI packages
329
+ │ ├─ New-BrowserExtensionPackage.ps1 browser-extension force-install (opt-in)
330
+ │ ├─ New-WindowsFeaturePackage.ps1 optional features / capabilities (opt-in)
331
+ │ ├─ New-DriverPackage.ps1 driver packages, pnputil staging (opt-in)
332
+ │ ├─ Get-DriverSignatureInfo.ps1 driver trust classifier (signed? kernel? deployable?)
333
+ │ │ gates + deliverables
334
+ │ ├─ Invoke-PsadtPreflight.ps1 pre-flight GREEN/RED gate (Phase 5, 10 checks)
335
+ │ ├─ Invoke-PsadtSystemTest.ps1 SYSTEM test (Phase 6)
336
+ │ ├─ Invoke-PsadtPackage.ps1 build the .intunewin (Phase 7, named + verified)
337
+ │ ├─ New-PsadtReport.ps1 HTML dossier (Phase 8, always)
338
+ │ │ intune / graph
339
+ │ ├─ New-PsadtEntraApp.ps1 Entra app bootstrap (WAM)
340
+ │ ├─ Get-GraphToken.ps1 app-only Graph token (cert / DPAPI)
341
+ │ ├─ Test-PsadtIntuneAccess.ps1 access verdict (roles, capabilities, expiry)
342
+ │ ├─ Invoke-IntuneWin32Upload.ps1 direct upload (Phase 9)
343
+ │ ├─ Invoke-IntuneAppAssignment.ps1 group assignment (Phase 10, opt-in)
344
+ │ ├─ New-IntuneTrustedCertPolicy.ps1 Custom OMA-URI cert policy (self-contained)
345
+ │ ├─ New-IntuneFirewallPolicy.ps1 firewall-rule policy (self-contained)
346
+ │ ├─ _GraphCommon.ps1 shared Graph helpers (retry, errors, token roles)
347
+ │ └─ _GraphInteractive.ps1 shared WAM sign-in
348
+ ├─ references/
349
+ │ ├─ README.md the reference map (label -> file)
350
+ │ ├─ phases-0-6.md · phases-7-12.md the twelve phases
351
+ │ ├─ appendix-a-errors.md … -q-drivers.md one file per appendix
352
+ │ ├─ switch-catalog/ engine defaults + JSON schema (App. L.0)
353
+ │ ├─ Report-Template.html the fixed dossier template
354
+ │ └─ app-registration.md THE Graph permission matrix + manual portal route
355
+ └─ tests/ Pester suite, 552 tests
356
+ ```
357
+
358
+ Machine-local state lives outside the skill folder:
359
+
360
+ ```
361
+ %LOCALAPPDATA%\psadt-deploy\ ($env:PSADT_DEPLOY_HOME overrides)
362
+ ├─ config.json settings incl. the optional intune.* block
363
+ ├─ secret.dpapi DPAPI client secret (only without cert auth)
364
+ └─ tools/ IntuneWinAppUtil.exe + WinGet module
365
+ ```
366
+
367
+ And per package, next to `Invoke-AppDeployToolkit.ps1`:
368
+
369
+ ```
370
+ psadt-package.json identity · gate decisions · research · results · artifacts
371
+ ```
372
+
373
+ ## Status
374
+
375
+ In active use for the full build → package → test → dossier workflow, with the direct Graph upload
376
+ verified against a live tenant. The helper scripts are covered by 503 Pester tests.
377
+
378
+ One open point, honestly: **the driver `pnputil` exit-code semantics are documented, not verified here.**
379
+ `0` / `259` / `3010` and the two `0xE...` failures come from Microsoft's documentation; confirming them
380
+ against `setupapi.dev.log` on a DEV VM with a real vendor-signed and a real Microsoft-signed driver is
381
+ still open.
382
+
383
+ ## Security
384
+
385
+ This skill installs software as SYSTEM, researches on the open web, and writes to an Intune tenant
386
+ through an Entra app with admin consent. [`SECURITY.md`](SECURITY.md) states that risk surface next to
387
+ the control that already covers each part of it — the dry-run-before-execute rule, the three-valued
388
+ access check, never-delete, role assertion before the first write, certificate before DPAPI secret,
389
+ the config home outside the skill folder, and the self-containment rule for anything that ships to a
390
+ test client. Each control names the file that implements it and the test that enforces it, so a review
391
+ can check the claims rather than take them.
392
+
393
+ Two deliberate non-features are explained there as well: the skill does **not** declare
394
+ `allowed-tools` (that field pre-approves tools, it does not restrict them), and content fetched during
395
+ research is treated as data, never as instructions — see
396
+ [`references/research-trust.md`](references/research-trust.md).
397
+
398
+ ## Roadmap
399
+
400
+ Designed and waiting to be built:
401
+
402
+ - **Sync finished packages to a GitHub repo** — a setup option (`output.target` = `local` / `git` / `both`)
403
+ to push the per-app artifacts (`.intunewin`, dossier, detection, logo) to a Git repo instead of, or in
404
+ addition to, a local folder — versioned and shareable. Will need **Git LFS** for large `.intunewin` files
405
+ (GitHub's 100 MB per-file limit).
406
+
407
+ Have a request? Open an issue.
408
+
409
+ ## Contributing
410
+
411
+ Issues and pull requests are welcome. Keep `SKILL.md`, the references and the docs in **English**. The only
412
+ non-English content is the generated end-user output (the Intune dossier and the Company-Portal app
413
+ description), whose language follows the `language.dossier` config value — **default German**, but
414
+ configurable per machine.
415
+
416
+ Two conventions worth knowing before you send a patch: generated `.ps1` content is **7-bit ASCII** (the
417
+ pre-flight fails on non-ASCII without a BOM), and anything that lands in a package's output folder must be
418
+ **self-contained** — it gets copied to test clients that have no skill installed.
419
+
420
+ ## License
421
+
422
+ [MIT](LICENSE) © Patrick Taubert, PHAT Consulting GmbH
423
+
424
+ ## Acknowledgements
425
+
426
+ - [PSAppDeployToolkit](https://github.com/PSAppDeployToolkit/PSAppDeployToolkit)
427
+ - [Microsoft Win32 Content Prep Tool](https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool)
428
+ - [`Invoke-CommandAs`](https://github.com/mkellerman/Invoke-CommandAs)
429
+ - README structure inspired by [ComposioHQ/awesome-claude-skills](https://github.com/ComposioHQ/awesome-claude-skills)
430
+
431
+ ## Changelog
432
+
433
+ The two most recent releases are below. **[CHANGELOG.md](CHANGELOG.md)** carries the complete history,
434
+ every release since 0.1.0, and nothing is ever removed from it - this section is a window onto it, not a
435
+ second copy to keep in sync.
436
+
437
+ ### 0.35.0 - 2026-09-17
438
+ - **Added: `scripts/New-ExePackage.ps1`** - the EXE family (Inno, NSIS, electron-builder) had no
439
+ generator, so every such package was hand-scaffolded. It now generates hooks that resolve the
440
+ uninstaller from the ARP entry at run time, WAIT for the app to actually disappear instead of
441
+ trusting an exit code, and detect with a version floor over the ARP entry and the binary.
442
+ - **Added: every action snapshots the installed-application entry, successful Installs included** -
443
+ including `InstallLocation`, `QuietUninstallString` and the property types PSADT hands the launcher,
444
+ surfaced as `InstalledAppFacts`. The old dump ran only when detection contradicted an action, so the
445
+ strings a resolver hook has to match were never on disk and were guessed at, one VM run per guess.
446
+ - **Added: fail-fast** - a red Install or Uninstall now skips Reinstall/Repair/FinalUninstall, which
447
+ could only re-prove the same failure. Each of four RED Firefox runs had spent ~3 minutes doing so.
448
+ - **Added: pre-flight `AsyncUninstall`** warns when an uninstall trusts the exit code of a vendor EXE
449
+ (the NSIS family relaunches from `%TEMP%` and returns instantly: 116 ms, exit 0, nothing deleted).
450
+ - **Changed: readable phase names** on the progress window (`SystemTaskCanary` -> "Probing: can
451
+ anything run as SYSTEM?"); the ids stay, because every consumer keys on them.
452
+ - **Changed: guest preparation dropped the always-failing WMI salvage pass and polls instead of
453
+ sleeping** - fixed overhead per run 139 s -> 81 s.
454
+ - **Fixed: a `GREEN_PARTIAL` run satisfied the upload gate** in `New-PsadtReport.ps1`.
455
+
456
+ Verified on three applications never packaged here: Audacity 4.0.0 (MSI) 12:12, VS Code 1.138.0
457
+ (Inno) 22:10, draw.io 31.4.5 (NSIS) **10:49 with a single VM run** - all GREEN on the first gate
458
+ attempt, against ~70 min and no gate at all for Firefox beforehand. Suite 631 -> 657.
459
+
460
+ ### 0.34.2 - 2026-09-16
461
+ - **Added: `tests/SiteFigures.Tests.ps1`** - the landing page's stat tiles are now derived from the
462
+ repository and compared, instead of being hand-maintained and unchecked. A reader found the page
463
+ claiming 19 installer engines in the tile and "1 of 14" in the Phase 2 step right below it; the
464
+ script count had also been one behind since 0.33.0. index.html lives on gh-pages, so the suite had
465
+ never seen it. The guard reads it out of that ref, skips itself when the ref is absent, and the
466
+ workflow fetches it so CI checks it for real. Both stale figures fixed. Suite 624 -> 631.
467
+
468
+ ### 0.34.1 - 2026-09-16
469
+ - **Changed: SKILL.md now names `-TrustedPublisherCert` at Phase 6**, with `-Scenarios` for iteration and
470
+ `STOP.txt` for cancelling. 0.34.0 documented the certificate in phase 6.1 only, because the control
471
+ plane had 55 bytes of headroom against its 5000-token budget - the wrong trade for a failure that is
472
+ expensive and silent, since an agent that never opens 6.1 repeats the 25-minute timeout blind. Room was
473
+ made the way the budget test prescribes: `rule:author-version-changelog` and `rule:start-menu-only`
474
+ moved to `references/conventions.md`, which already carried their full text and which SKILL.md routes
475
+ to. Both are editorial rules - their failure costs a doc edit or a stray desktop icon, not a
476
+ deployment. Control plane 17348 / 17500 bytes, headroom 55 -> 152. Suite 624, unchanged.
477
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "psadt-deploy-skill",
3
- "version": "0.34.2",
3
+ "version": "0.35.0",
4
4
  "description": "Installer for the psadt-deploy Claude Code skill: build, test and deploy PSADT v4.x Intune Win32 packages.",
5
5
  "keywords": [
6
6
  "psadt",