0. 背景
Windowsユーザーなら、誰もが一度は味わったことがあるだろう。
アップデートのたびに崩れる、自分だけの最適化環境。
右クリックメニューは改悪され、スタートメニューには広告の波が押し寄せる。
OneDriveは勝手に有効化され、不要なアプリを勝手にインストールされ、
それらがCPUとメモリのリソースを食い荒らす。
レジストリで積み上げた調整も、気づけば初期化されている。
ユーザーのあらゆる努力は、Microsoftによって容赦なく崩される。
まるで賽の河原の石積みだ。
「じゃあMacやLinuxに移ればいい」と言う声もある。
だが、自分の使いたいアプリが使えないなら、PCは単なる箱でしかない。
Windowsを使い続けながら、自分の環境をどう守るか。
それは、Windowsユーザーの永遠の課題だ。
1. はじめに:Windows 11の性根を叩き直す
Windows Updateは必要不可欠だ。
セキュリティのためには避けられない。
しかし、その代償は大きい。
最適化したはずのレジストリ設定が、あっさり初期化されてしまう。
- Windows10のデザインに戻した右クリックメニュー
- スタートメニューやエクスプローラーに出てくる広告やおすすめのオフ設定
- 開発効率を高めるための細かなチューニング
これらがリセットされるたびに手作業で戻すのは、限りある人生の時間の無駄だ。
そこで私が作ったのは、
「あるべきレジストリ設定」をCSVで定義し、変更があれば即座に検知して復元するPowerShellスクリプト「Reg-Guardian」 だ。
この記事のゴールは以下の3つ。
- Windows Updateに怯える日々からの解放
- クリーンインストールや複数PCでも一瞬で“自分の環境”を復元
- PowerShellを使った実践的な環境維持・管理手法の習得
2. 成果物:Reg-Guardian
ファイル構成
C:\Tools\Reg-Guardian(任意のディレクトリ、任意のフォルダ名でよい)
│
├── reg-guardian-cli.ps1 # 本体スクリプト
├── reg-list.csv # "あるべき設定"を記述するリスト
└── logs/ # 実行ログ保存用フォルダ
主な機能
-
宣言的な設定管理:
reg-list.csvに「こうあるべき」を書くだけ - 変更の自動検出: CSVと現在のレジストリを比較し、余計な変更を洗い出す
- 対話的な修正: 改変が見つかればリストアップし、承認後に復元
- 自動化: タスクスケジューラでログイン時に実行し、常に監視
3. 使い方
注意事項
本ツールは CSV に記述された任意のレジストリキーを処理できる。
つまり、システム領域を含むキーも操作可能だ。
誤操作は重大な障害につながるため、利用はユーザー環境のカスタマイズに限定することを強く推奨する。
主に対象とすべき範囲
-
HKEY_CURRENT_USER\...
(例: エクスプローラーの表示設定、右クリックメニューの挙動など) -
HKEY_LOCAL_MACHINE\SOFTWARE\Policies\...
(例: Windows Update のバージョン固定、広告表示の抑制など)
操作対象から外すべき範囲
-
HKEY_LOCAL_MACHINE\SYSTEM\...
(ドライバ、サービス、カーネル関連) -
HKEY_LOCAL_MACHINE\HARDWARE\...
(ハードウェア検出や初期化に関わる領域) -
HKEY_USERS\.DEFAULT\...
(システム起動時の既定ユーザー設定)
これらは OS の安定動作に直結するため、本ツールで触れるべきではない。
レジストリ操作は常にリスクを伴う。実行は自己責任で行い、必ず事前にバックアップを取ること。
ステップ1: ファイルの配置
C:\Tools\Reg-Guardian (任意のディレクトリ、フォルダ名でよい)
を作成し、その中に
-
reg-guardian-cli.ps1(記事末尾のコードをコピー) -
reg-list.csv(後述)
を作成。
logs フォルダは初回実行時に自動生成される。
ステップ2: reg-list.csv の編集
このCSVが心臓部だ。
無効化したい機能や適用したい設定を記述する。
設定例:
Path,Name,Type,Value
#【 操作性向上 】
# Windows 11 の右クリックメニューを Windows 10のものに戻す設定
"HKEY_CURRENT_USER\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32",,REG_SZ,
# 右クリックメニュー表示の遅延時間を短縮する設定 (ミリ秒)
"HKEY_CURRENT_USER\Control Panel\Desktop","MenuShowDelay",REG_SZ,200
# 【 広告オフ 】
# Windows 10/11 のスタートメニューやエクスプローラーに表示される広告をオフにする設定
"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager","SubscribedContent-310093Enabled",REG_DWORD,0
# 個人用設定 > ロック画面 > ロック画面にトリビアやヒントなどの情報を非表示にする
"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager","SubscribedContent-338387Enabled",REG_DWORD,0
# Windowsの使用中に表示されるヒントや提案(「Windows を最大限に活用し、このデバイスの設定を完了する方法を提案する」など)を無効にする設定
"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager","SubscribedContent-338388Enabled",REG_DWORD,0
"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager","SubscribedContent-338389Enabled",REG_DWORD,0
# 個人用設定 > ヒント、ショートカット、新しいアプリなどのおすすめを非表示にする
"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced","Start_IrisRecommendations",REG_DWORD,0
# 【 不要なサービスの起動阻止】
# OneDriveのセットアップ要求を無効にする設定
"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\OneDrive","DisableFileSyncNGSC",REG_DWORD,1
# 【 Windows自動アップデートの阻止 】
# Windowsのメジャーバージョンを固定する設定。
"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate","TargetReleaseVersion",REG_DWORD,1
"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate","TargetReleaseVersionInfo",REG_SZ,24H2
ステップ3: スクリプトの実行
PowerShellを管理者として実行し、以下を入力する。
cd C:\Tools\Reg-Guardian
.\reg-guardian-cli.ps1
ステップ4: ログイン時に自動実行(推奨)
Windowsアップデートのたびに、毎回スクリプトを手動で実行するのは非効率だ。
タスクスケジューラに登録し、自動化してしまおう。
-
タスクスケジューラを開く
-
[全般]タブ:
- 名前:
Reg-Guardianなど任意の名前 - ✅ 「最上位の特権で実行する」に必ずチェックを入れる。 (← HKLMを書き換えるための最重要ポイント)
- 名前:
-
[トリガー]タブ:
- 「新規」をクリックし、「タスクの開始」を**「ログオン時」**に設定する
-
[操作]タブ:
- 「新規」をクリック
- 操作: 「プログラムの開始」
- プログラム/スクリプト:
powershell.exe - 引数の追加:
-ExecutionPolicy Bypass -File "C:\Tools\Reg-Guardian\reg-guardian-cli.ps1"
5. まとめ
「Reg-Guardian」は、Windows Update による環境破壊から守るための武器だ。
CSVで理想状態を定義し、スクリプトで強制する――その発想は他の管理にも応用できる。
ただし、レジストリ操作は常にリスクを伴う。検証環境で試し、バックアップを忘れないこと。
Windowsに不満を抱えるユーザーにとって、環境を守る一手となれば幸いだ。
6. reg-guardian-cli.ps1 全コード
以下をコピーしたら「ステップ1: ファイルの配置」に戻る
<#
.SYNOPSIS
reg-list.csv に基づいてレジストリ設定を監視し、変更が検出された場合に復元する対話型スクリプト。
.DESCRIPTION
Windows Updateや他のアプリケーションによって意図せず変更されたレジストリ設定を、
あらかじめ定義したリスト(reg-list.csv)の状態に復元することを目的とします。
スクリプトは実行時にレジストリをチェックし、差分があった場合にユーザーに復元を確認します。
処理内容はログファイルに出力され、古いログは自動的に削除されます。
.NOTES
Author: Reg-Guardian-CLI
Version: 1.0
前提条件:
- このスクリプトは管理者権限で実行する必要があります。
- タスクスケジューラ等で「最上位の特権で実行する」設定で、ログイン時に実行されることを想定しています。
- スクリプトと同じディレクトリに、監視対象を記述した "reg-list.csv" が必要です。
#>
#---------------------------------------------------------------------------------------------
# 設定セクション
#---------------------------------------------------------------------------------------------
# 監視・復元対象のレジストリ情報を記載したCSVファイル名
$RegListFileName = "reg-list.csv"
# ログファイルを保存するディレクトリ名
$LogDirectoryName = "logs"
# 保持するログファイルの最大世代数
$LogRetentionCount = 10
# このスクリプトが設置されているディレクトリのパスを取得
$ScriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Definition
# ログディレクトリのフルパスを生成
$LogDirectory = Join-Path -Path $ScriptDirectory -ChildPath $LogDirectoryName
#---------------------------------------------------------------------------------------------
# 初期化セクション
#---------------------------------------------------------------------------------------------
# ログディレクトリが存在しない場合は作成する
if (-not (Test-Path -Path $LogDirectory)) {
New-Item -Path $LogDirectory -ItemType Directory | Out-Null
}
# タイムスタンプ付きのログファイルパスを生成
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$LogFileName = "reg-guardian-log_$timestamp.log"
$LogFilePath = Join-Path -Path $LogDirectory -ChildPath $LogFileName
#---------------------------------------------------------------------------------------------
# 関数定義セクション
#---------------------------------------------------------------------------------------------
<#
.SYNOPSIS
古いログファイルを削除し、ログの世代管理を行います。
.DESCRIPTION
ログディレクトリ内のログファイル数をチェックし、設定変数 $LogRetentionCount で
定義された最大数を超えた場合に、最も古いログファイルから順に削除します。
#>
Function Manage-LogRotation {
# ログファイルを取得し、最終書き込み日時で降順(新しい順)にソート
$logFiles = Get-ChildItem -Path $LogDirectory -Filter "reg-guardian-log_*.log" | Sort-Object LastWriteTime -Descending
if ($logFiles.Count -gt $LogRetentionCount) {
# 保持数を超えた分の古いログファイルを選択
$filesToDelete = $logFiles | Select-Object -Skip $LogRetentionCount
foreach ($file in $filesToDelete) {
Write-Host "古いログファイルを削除します: $($file.Name)" -ForegroundColor Gray
Remove-Item -Path $file.FullName -Force
}
}
}
<#
.SYNOPSIS
メッセージをコンソールとログファイルの両方に出力します。
.PARAMETER Message
出力するメッセージ文字列。
.PARAMETER ForegroundColor
コンソールに表示する際の文字色を指定します。デフォルトは "White" です。
.PARAMETER NoNewLine
Trueに設定すると、コンソール出力時にメッセージの末尾で改行しません。
#>
Function Write-Log {
Param(
[string]$Message,
[string]$ForegroundColor = "White",
[bool]$NoNewLine = $false
)
$logTimestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$logTimestamp] $Message"
# ログファイルにUTF8で追記
Add-Content -Path $LogFilePath -Value $logEntry -Encoding UTF8
if ($NoNewLine) {
Write-Host $Message -ForegroundColor $ForegroundColor -NoNewline
} else {
Write-Host $Message -ForegroundColor $ForegroundColor
}
}
<#
.SYNOPSIS
エラーメッセージをコンソール(赤字)とログファイルに出力します。
.PARAMETER Message
出力するエラーメッセージ文字列。
#>
Function Write-LogError {
Param([string]$Message)
$logTimestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$logTimestamp] [ERROR] $Message"
Add-Content -Path $LogFilePath -Value $logEntry -Encoding UTF8
Write-Host "[ERROR] $Message" -ForegroundColor Red
}
<#
.SYNOPSIS
標準的なレジストリパスをPowerShellが認識できるPSPath形式に変換します。
.PARAMETER RegPath
"HKEY_LOCAL_MACHINE\SOFTWARE\..." のような形式のレジストリパス。
.OUTPUTS
"HKLM:\SOFTWARE\..." のような形式のPSPath。対応していないハイブの場合は $null を返します。
#>
Function Get-PSPathFromRegistryPath {
Param([string]$RegPath)
if (-not $RegPath.Contains('\')) {
Write-LogError "無効なレジストリパスです: $RegPath"
return $null
}
$hive = $RegPath.Split('\')[0]
$pathSuffix = $RegPath.Substring($RegPath.IndexOf('\') + 1)
switch ($hive.ToUpper()) {
"HKEY_CURRENT_USER" { return "HKCU:\$pathSuffix" }
"HKEY_LOCAL_MACHINE" { return "HKLM:\$pathSuffix" }
"HKEY_CLASSES_ROOT" { return "HKCR:\$pathSuffix" }
"HKEY_USERS" { return "HKU:\$pathSuffix" }
"HKEY_CURRENT_CONFIG"{ return "HKCC:\$pathSuffix" }
default {
Write-LogError "未対応のレジストリHiveです: $hive"
return $null
}
}
}
<#
.SYNOPSIS
指定されたレジストリ値が期待値と一致するかどうかを検証します。
.PARAMETER Path
検証するレジストリキーのPSPath。
.PARAMETER Name
検証するレジストリ値の名前。"(既定)"の場合は空文字または$nullを指定します。
.PARAMETER ExpectedValue
レジストリ値が持つべき期待値。
.PARAMETER Type
レジストリ値の型 (REG_SZ, REG_DWORDなど)。型に応じて適切な比較を行います。
.OUTPUTS
[bool] 値が期待値と一致する場合は $true、それ以外の場合は $false を返します。
#>
Function Test-RegistryValue {
Param (
[string]$Path,
[string]$Name,
[string]$ExpectedValue,
[string]$Type
)
$registryName = if ([string]::IsNullOrEmpty($Name)) { '(Default)' } else { $Name }
# 現在のレジストリ値を取得。存在しない場合はエラーを表示せず $null を返す
$currentValue = Get-ItemPropertyValue -Path $Path -Name $registryName -ErrorAction SilentlyContinue
if ($null -eq $currentValue) {
# (既定)の値で、かつ期待値が空文字の場合、キーが存在すれば一致とみなす
if ($registryName -eq '(Default)' -and [string]::IsNullOrEmpty($ExpectedValue)) {
return (Test-Path $Path)
}
return $false
}
# 型に応じて比較方法を切り替え
switch ($Type.ToUpper()) {
"REG_DWORD" { try { return ([long]$currentValue -eq [long]$ExpectedValue) } catch { return $false } }
"REG_SZ" { return ($currentValue -eq $ExpectedValue) }
"REG_EXPAND_SZ" { return ($currentValue -eq $ExpectedValue) }
default { return ($currentValue.ToString() -eq $ExpectedValue.ToString()) }
}
}
<#
.SYNOPSIS
指定された値でレジストリを設定(書き込み・上書き)します。
.DESCRIPTION
指定されたパスにレジストリキーが存在しない場合は自動的に作成します。
.PARAMETER Path
設定対象のレジストリキーのPSPath。
.PARAMETER Name
設定するレジストリ値の名前。
.PARAMETER Type
設定するレジストリ値の型。
.PARAMETER Value
設定する値。
.OUTPUTS
[bool] 設定に成功した場合は $true、失敗した場合は $false を返します。
#>
Function Set-RegistryValue {
Param (
[string]$Path,
[string]$Name,
[string]$Type,
[object]$Value
)
try {
if (-not (Test-Path -Path $Path)) {
# パスが存在しない場合は強制的に作成
New-Item -Path $Path -Force | Out-Null
}
$registryName = if ([string]::IsNullOrEmpty($Name)) { '(Default)' } else { $Name }
# CSVの型名を New-ItemProperty が要求する型名に変換
$psType = switch ($Type.ToUpper()) {
"REG_DWORD" { "DWord" }
"REG_SZ" { "String" }
"REG_EXPAND_SZ" { "ExpandString" }
default {
Write-LogError "サポートされていないレジストリタイプ: $Type。スキップします。"
return $false
}
}
# レジストリ値を作成または上書き。エラー発生時は例外をスローする
New-ItemProperty -Path $Path -Name $registryName -Value $Value -PropertyType $psType -Force -ErrorAction Stop | Out-Null
Write-Log -Message " -> 復元成功: Path='$Path', Name='$registryName', Value='$Value'" -ForegroundColor DarkGreen
return $true
}
catch {
Write-LogError "レジストリ設定エラー: $($_.Exception.Message). Path='$Path', Name='$registryName'"
return $false
}
}
#---------------------------------------------------------------------------------------------
# メイン処理
#---------------------------------------------------------------------------------------------
# 1. ログローテーションの実行と起動ログの記録
Manage-LogRotation
Write-Log -Message "--- Reg-Guardian-CLI 起動 ---" -ForegroundColor Cyan
Write-Log -Message "スクリプトパス: $ScriptDirectory"
Write-Log -Message "ログファイル: $LogFilePath"
# 2. レジストリリスト(CSV)の読み込み
$regListPath = Join-Path -Path $ScriptDirectory -ChildPath $RegListFileName
if (-not (Test-Path -Path $regListPath)) {
Write-LogError "`$RegListFileName` ('$RegListFileName') が見つかりません。"
Write-Log -Message "エンターキーを押して終了します..." -NoNewLine $true; [void]$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp"); Write-Host ""
Exit
}
try {
# CSVを読み込み、Pathが 'HKEY' で始まる有効な行のみを抽出
$regList = Import-Csv -Path $regListPath -Encoding UTF8 | Where-Object { $_.Path -like 'HKEY*' }
Write-Log -Message "`$RegListFileName` を読み込みました。チェック対象: $($regList.Count)件"
} catch {
Write-LogError "`$RegListFileName` の読み込みまたはパースに失敗しました。"
Write-Log -Message "エンターキーを押して終了します..." -NoNewLine $true; [void]$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp"); Write-Host ""
Exit
}
# 3. レジストリ設定のチェック
$restoreList = [System.Collections.Generic.List[object]]::new()
$totalCount = $regList.Count
$counter = 1
Write-Log -Message "`n--- レジストリ設定のチェック開始 ---"
foreach ($item in $regList) {
# CSVの行データが不完全な場合はスキップ
if (-not $item.Path -or -not $item.Type) {
Write-LogError "[$($counter)/$($totalCount)] CSVの行が無効です。PathまたはTypeがありません。スキップします。"; $counter++; continue
}
$displayName = if ([string]::IsNullOrEmpty($item.Name)) { '(Default)' } else { $item.Name }
Write-Log -Message "[$($counter)/$($totalCount)] Checking: Path='$($item.Path)', Name='$displayName'" -ForegroundColor Gray
$psPath = Get-PSPathFromRegistryPath -RegPath $item.Path
if (-not $psPath) { $counter++; continue }
# 現在のレジストリ値とCSVの期待値を比較
if (-not (Test-RegistryValue -Path $psPath -Name $item.Name -ExpectedValue $item.Value -Type $item.Type)) {
# 変更を検出した場合、詳細をログに出力し、復元リストに追加
$registryNameForLog = if ([string]::IsNullOrEmpty($item.Name)) { '(Default)' } else { $item.Name }
$currentValue = Get-ItemPropertyValue -Path $psPath -Name $registryNameForLog -ErrorAction SilentlyContinue
$currentValueLog = if ($null -eq $currentValue) { "(値が存在しないか、セットされていません)" } else { "'$currentValue'" }
Write-Log -Message " -> [変更を検出]" -ForegroundColor Yellow
Write-Log -Message " -> 期待値: '$($item.Value)', 現在値: $currentValueLog" -ForegroundColor Yellow
$restoreList.Add($item)
} else {
# 変更がない場合
Write-Log -Message " -> [一致]" -ForegroundColor Green
}
$counter++
}
Write-Log -Message "--- チェック完了 ---`n"
# 4. 復元処理の実行
if ($restoreList.Count -eq 0) {
# 変更が検出されなかった場合はスクリプトを終了
Write-Log -Message "レジストリの変更は検出されませんでした。" -ForegroundColor Green
Start-Sleep -Seconds 2
Exit
}
# 変更が検出された場合、ユーザーに復元の意思を確認
Write-Log -Message "レジストリの変更を検出しました!!reg-listの設定値で復元しますか? (y / N)" -ForegroundColor Red
$response = Read-Host
Add-Content -Path $LogFilePath -Value "ユーザー入力: $response" -Encoding UTF8
if ($response.ToLower() -eq 'y') {
# ユーザーが 'y' を選択した場合、復元処理を開始
Write-Log -Message "ユーザーが復元を選択しました。"
Write-Log -Message "`nレジストリの復元を開始します..." -ForegroundColor Green
$allSuccess = $true
foreach ($item in $restoreList) {
$psPath = Get-PSPathFromRegistryPath -RegPath $item.Path
if (-not (Set-RegistryValue -Path $psPath -Name $item.Name -Type $item.Type -Value $item.Value)) {
$allSuccess = $false
}
}
if ($allSuccess) {
Write-Log -Message "`nレジストリの復元が完了しました。" -ForegroundColor Green
} else {
Write-Log -Message "`nレジストリの復元中に一部エラーが発生しました。詳細はログを確認してください。" -ForegroundColor Red
}
# 復元後、設定を反映させるために再起動を促す
Write-Log -Message "今すぐ再起動しますか?(y / N)" -ForegroundColor Yellow
$rebootResponse = Read-Host
Add-Content -Path $LogFilePath -Value "ユーザー入力: $rebootResponse" -Encoding UTF8
if ($rebootResponse.ToLower() -eq 'y') {
Write-Log -Message "ユーザーが再起動を選択しました。システムを再起動しています..." -ForegroundColor Yellow
Write-Log -Message "--- スクリプト終了 ---"
Restart-Computer -Force
} else {
Write-Log -Message "再起動はスキップされました。"
Write-Log -Message "設定の反映のためには、再起動することをお勧めします。" -ForegroundColor Yellow
}
} else {
# ユーザーが復元をキャンセルした場合
Write-Log -Message "復元はスキップされました。スクリプトを終了します。"
}
# 5. 終了処理
# コンソールウィンドウがすぐに閉じてしまわないように、ユーザーのキー入力を待つ
Write-Log -Message "エンターキーを押して終了します..." -NoNewLine $true
[void]$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp")
Write-Host ""
Write-Log -Message "--- スクリプト終了 ---"
