ClickOnce で配布した C# アプリから PowerShell スクリプトだけを管理者権限で実行したい──現場ではよくある要件ですが、「アプリ自体を管理者で起動できない」「引数のエスケープが怖い」など、ハマりポイントも多いテーマです。本記事では、ClickOnce の制約を踏まえつつ、実践的な C# 実装パターンと注意点をまとめます。
前提:C# + ClickOnce から PowerShell を管理者権限で実行したい
この記事で想定しているシナリオは次のようなものです。
- クライアント PC にインストールされているのは、ClickOnce で配布された C# デスクトップアプリ(WinForms / WPF など)。
- アプリ本体は通常権限で動作してよいが、一部の処理だけは管理者権限が必要(レジストリ変更・サービス操作・ファイル配布など)。
- その権限が必要な処理は、PowerShell スクリプトとして用意しておき、C# から呼び出したい。
- スクリプトの引数(ArgumentList)は、ハードコードせずパラメータで渡したい。
結論から言うと、
- ClickOnce アプリ本体を「常に管理者として実行」することはできません。
- 代わりに、C# から別プロセスで
powershell.exe(またはpwsh.exe)を-Verb runas付きで起動し、その子プロセスに PowerShell スクリプトを実行させるのが基本パターンです。
ClickOnce と管理者権限の仕様を整理する
最初に、ClickOnce の制約をざっくり整理しておきます。
| やりたいこと | ClickOnce での可否 | ポイント |
|---|---|---|
| アプリ本体を常に管理者として起動する | ✕ 不可 | ClickOnce では requireAdministrator のような manifest 要求を付けられない |
| アプリ本体は通常権限、必要な処理だけ管理者権限で実行 | ◯ 可能 | ProcessStartInfo.Verb = "runas" などで別プロセスを昇格 |
| UAC プロンプトなしで自動昇格 | 基本的に ✕ | タスクスケジューラ / サービスなど OS 側の仕組みとセットで設計する必要あり |
つまり、ClickOnce と UAC の仕様上、
- アプリ本体は「普通のユーザーアプリ」として動かす
- 管理者が必要な処理だけ、別プロセスを昇格させて実行する
という二段構えで設計するのが現実的です。
基本戦略:アプリ本体は通常権限、PowerShell だけ昇格させる
よくある実装パターンは次の流れです。
- C# アプリから
ProcessStartInfoを使ってpowershell.exeあるいはpwsh.exeを起動する。 UseShellExecute = true、Verb = "runas"を指定して、「管理者として実行」を OS に依頼する。- ユーザーが UAC ダイアログで許可すると、昇格済み PowerShell プロセスが起動し、スクリプトが管理者権限で動く。
ポイントは以下の2つです。
- 昇格するのは PowerShell プロセスだけであり、ClickOnce アプリ本体は通常権限のまま。
- 引数を安全に組み立てて PowerShell に渡す実装が重要(クォート崩れ・特殊文字への対処)。
ここからは、実際に使える C# コード例を見ていきます。
C# 実装例 1:最小コードで PowerShell を管理者として起動する
まずは、もっともシンプルな「-File でスクリプトを渡す」パターンです。
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
public static class AdminPowerShellRunner
{
public static void RunPsAsAdmin(string scriptPath, params string[] scriptArgs)
{
if (string.IsNullOrWhiteSpace(scriptPath))
{
throw new ArgumentException("scriptPath is required.", nameof(scriptPath));
}
// 引数を最低限エスケープして "..." で囲む
string joinedArgs = string.Join(" ",
(scriptArgs ?? Array.Empty<string>())
.Select(EscapeArg));
string psArgs =
$"-NoProfile -ExecutionPolicy Bypass -File \"{scriptPath}\" {joinedArgs}";
var psi = new ProcessStartInfo
{
FileName = "powershell.exe", // PowerShell 5.x。PowerShell 7 は "pwsh.exe"
UseShellExecute = true, // runas を使うには true が必須
Verb = "runas", // 「管理者として実行」
Arguments = psArgs
};
try
{
Process.Start(psi); // UAC プロンプトが表示される
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
// ユーザーが UAC をキャンセルした場合
// ログだけ残して握りつぶすなど、アプリの仕様に合わせて処理
}
}
private static string EscapeArg(string arg)
{
if (arg == null)
{
return "\"\"";
}
// 既存の " を \" に変換してから全体を "..." で囲む
return "\"" + arg.Replace("\"", "\\\"") + "\"";
}
}
呼び出し例:
// 例: ユーザー名と Force オプションを渡す
AdminPowerShellRunner.RunPsAsAdmin(
scriptPath: @"C:\Scripts\DoSomething.ps1",
"-Name", "Taro Yamada",
"-Force"
);
コードのポイント解説
UseShellExecute = trueVerb = "runas"を使えるのはUseShellExecute = trueのときだけです。ArgumentListプロパティ(.NET 5+)はUseShellExecute = falseのときしか使えないため、昇格実行時はArguments文字列を自前で組み立てる必要があります。
-ExecutionPolicy Bypass- 端末ごとの実行ポリシー設定によるエラーを減らします。
- ポリシー運用上のルールがある組織では、ここを変更する(あるいは署名付きスクリプト+RemoteSigned など)方針も検討してください。
EscapeArgメソッド- 引数中の
"を\"に置き換えてから全体を"..."で囲む、最低限のエスケープです。 - スペースや日本語を含む引数も、そのまま渡せます。
- 引数中の
PowerShell 起動オプションの意味
| オプション | 指定値 | 役割 |
|---|---|---|
| -NoProfile | (フラグのみ) | ユーザーのプロファイルやカスタムプロファイルを読み込まず、環境差を減らす |
| -ExecutionPolicy | Bypass | 実行ポリシーを一時的に上書きし、スクリプト実行エラーを回避 |
| -File | “C:\Scripts\DoSomething.ps1” | 指定したスクリプトファイルを実行する |
| スクリプト引数 | -Name “Taro” など | param() ブロックで受け取る PowerShell 側のパラメータ |
C# 実装例 2:-EncodedCommand で引数を堅牢に渡す
上の実装でも最低限動きますが、引数に
- 引用符
"と'が混在 &、|、>などシェル的な記号
が含まれてくると、クォート崩れに悩まされがちです。 そこで、C# 側で PowerShell コマンド全体を組み立ててから、UTF-16LE で Base64 エンコードして -EncodedCommand で渡すと、かなり堅牢になります。
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
public static class AdminPowerShellRunner
{
public static void RunPsAsAdminEncoded(string scriptPath, params string[] scriptArgs)
{
if (string.IsNullOrWhiteSpace(scriptPath))
{
throw new ArgumentException("scriptPath is required.", nameof(scriptPath));
}
// PowerShell のシングルクォートルールに沿ってエスケープ
string escapedScriptPath = scriptPath.Replace("'", "''");
string argList = string.Join(" ",
(scriptArgs ?? Array.Empty<string>())
.Select(a => "'" + (a ?? string.Empty).Replace("'", "''") + "'"));
// 実際に PowerShell で実行させたいコマンド文字列
string psCommand = $"& '{escapedScriptPath}' {argList}";
// UTF-16LE (Encoding.Unicode) でエンコードしてから Base64 化
string base64 = Convert.ToBase64String(Encoding.Unicode.GetBytes(psCommand));
var psi = new ProcessStartInfo
{
FileName = "powershell.exe",
UseShellExecute = true,
Verb = "runas",
Arguments = $"-NoProfile -ExecutionPolicy Bypass -EncodedCommand {base64}"
};
try
{
Process.Start(psi);
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
// UAC キャンセル時の処理
}
}
}
呼び出し例:
AdminPowerShellRunner.RunPsAsAdminEncoded(
scriptPath: @"C:\Scripts\DoSomething.ps1",
"-Name", "O'Hara", // シングルクォートを含んでも安全
"-Comment", "A & B | C > D"
);
-File と -EncodedCommand の比較
| 項目 | -File 方式 | -EncodedCommand 方式 |
|---|---|---|
| 実装のシンプルさ | ◯ シンプル | △ 多少複雑 |
| 引数の安全性 | △ クォート設計に注意 | ◎ Base64 で安定 |
| トラブルシュートのしやすさ | ◯ コマンドラインがそのまま見える | △ Base64 をデコードする一手間が必要 |
| 特殊文字・複雑な引数 | △ 慎重なエスケープが必要 | ◎ ほとんど気にしなくてよい |
実務では、
- スクリプト引数が素直(日本語+スペース程度)なら -File 方式
- ユーザー入力をそのまま渡す、URL や JSON、SQL などを含むなら -EncodedCommand 方式
というように使い分けると運用しやすくなります。
PowerShell 側のスクリプト例:param ブロックで受け取る
C# 側から渡した引数は、PowerShell スクリプト内で param ブロックとして受け取ります。
param(
[Parameter(Mandatory = $true)]
[string]
$Name,
[switch]
$Force ) Write-Host “Name : $Name” Write-Host “Force : $Force” if ($Force) { Write-Host “Force オプションが指定されました。” } # ここから管理者権限が必要な処理 # 例: サービス再起動 # Restart-Service -Name “MyService” -Force
C# 側からは、次のように呼び出します。
AdminPowerShellRunner.RunPsAsAdmin(
scriptPath: scriptPathFromApp,
"-Name", userName,
"-Force" // [switch] は値なしで指定
);
PowerShell 側にあらかじめ「どんな引数が来るか」をきちんと定義しておくと、アプリ側も無理なくパラメータを構築できます。
ClickOnce 特有のパスとデプロイの注意点
ClickOnce では、実行ファイルや付属ファイルがユーザープロファイル配下の特殊なパスに配置されます。そのため、スクリプトへのパス指定に気を付けないと、ローカル開発環境では動くのに、本番ではファイルが見つからない…という事故が起きがちです。
スクリプトの配置パターン
| 配置場所 | メリット | 注意点 |
|---|---|---|
| アプリと同じフォルダ(ClickOnce のアプリケーションフォルダ) | バージョンごとにスクリプトも更新される | ApplicationDeployment.CurrentDeployment.DataDirectory などで場所を解決する必要あり |
C:\ProgramData\MyApp\Scripts など固定フォルダ | 他のツールからも参照しやすい | 初回セットアップや権限設定が別途必要 |
| ネットワーク共有フォルダ | スクリプトを一括更新できる | 実行ポリシー / UNC パスの制限 / オフライン時の動作などを要確認 |
ClickOnce のアプリケーションフォルダにスクリプトを一緒に配布する場合、C# からのパス取得例は次のようになります(WinForms / WPF 共通)。
using System;
using System.Deployment.Application;
using System.IO;
string GetScriptPathInClickOnceApp(string scriptFileName)
{
// ClickOnce でデプロイされている場合
if (ApplicationDeployment.IsNetworkDeployed)
{
string dataDir = ApplicationDeployment.CurrentDeployment.DataDirectory;
return Path.Combine(dataDir, "Scripts", scriptFileName);
}
// 開発環境(デバッグ中など)は通常の実行ファイルフォルダ
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
return Path.Combine(baseDir, "Scripts", scriptFileName);
}
このようにして、
- デバッグ環境:
\bin\Debug\net48\Scripts\DoSomething.ps1 - ClickOnce 本番環境:
C:\Users\<user>\AppData\Local\Apps\2.0\...\Data\Scripts\DoSomething.ps1
といった差を意識せずに扱えるようにしておくと、運用がずっと楽になります。
無人実行や UAC 回避が必要な場合の設計パターン
ユーザー操作なしで管理者権限の処理を行いたい場合、Verb = "runas" だけでは実現できません。UAC プロンプトはユーザーの明示的な承認を求める仕組みであり、アプリ側で勝手にスキップすることはできないからです。
このような要件がある場合は、OS 側の機能と組み合わせる設計に切り替えましょう。
パターン1:タスクスケジューラ(最高権限タスク)を事前配布
- 管理者があらかじめ「最高権限で実行」のタスクを登録しておく。
- ClickOnce アプリは、そのタスクを
schtasks.exe /Run /TN "タスク名"で起動するだけにする。 - タスク登録時の資格情報や権限は、IT 管理者側で管理。
この方式なら、エンドユーザー側では UAC プロンプトを見ずに、管理者権限の処理を実行できます(ただし、セキュリティ方針との整合性は必ず確認してください)。
パターン2:Windows サービスに処理を委譲
- ローカルシステム(または特定のサービスアカウント)で動作する Windows サービスを用意。
- ClickOnce アプリは通常権限でサービスにリクエストを送り、サービス側で管理者権限が必要な処理を実行。
- 通信には、Named Pipe / TCP / WCF など任意のプロトコルを利用。
やや大掛かりになりますが、無人実行・スケジュール実行・ログ管理などをしやすいアーキテクチャです。
よくあるエラーとデバッグのポイント
PowerShell の昇格実行まわりで遭遇しがちな問題と、その原因・対処をまとめておきます。
| 症状 | 主な原因 | 対処の方向性 |
|---|---|---|
| 何も起きないように見える(PowerShell が動いた気がしない) | ユーザーが UAC をキャンセル / パス誤り | Win32Exception(NativeErrorCode = 1223)を捕捉しログ出力、Arguments をログに残して検証 |
| スクリプトの実行が実行ポリシーで拒否される | -ExecutionPolicy Bypass を付けていない、もしくは GPO でより強い制限 | 起動オプションを見直す、スクリプトの署名、組織のポリシーに合わせた設定を確認 |
| 指定したパスが見つからない(File not found) | ClickOnce の実行パスと開発環境のパスの違い | 実行時に解決したパスをログ出力、ApplicationDeployment.CurrentDeployment.DataDirectory を活用 |
| 引数がずれる / 日本語が切れる | クォートが崩れている、文字コードの取り扱いミス | -EncodedCommand 方式に切り替える、または引数エスケープ処理を見直す |
とくに ClickOnce 環境では、ユーザー環境ごとのパスの違いやポリシーの違いが原因になることが多いため、実際に投げたコマンドライン文字列をログに残しておくことを強くおすすめします。
セキュリティ・運用上の注意点
管理者権限で PowerShell を動かす以上、セキュリティ面にも十分配慮する必要があります。
- スクリプトの保護
- スクリプトに機微な情報(パスワードなど)をハードコードしない。
- スクリプトファイルへのアクセス権限(NTFS ACL)を最小限にする。
- 可能であればコード署名を行い、改ざん検知ができるようにする。
- アプリから渡す引数のバリデーション
- ユーザー入力をそのままスクリプトに渡す場合、想定外のコマンド注入が起きないよう検証。
-EncodedCommandを使っても、結局は渡した文字列が PowerShell で評価されることに注意。
- ログと監査
- 誰がいつ昇格処理を実行したか、アプリ側・スクリプト側双方でログに残す。
- イベントログへの出力や、組織の SIEM に連携するなど、運用設計もセットで考える。
- 最小権限の原則
- 本当に管理者権限が必要な処理だけを PowerShell に切り出し、それ以外は通常権限で行う。
- 「なんとなく管理者で動かしたほうが楽そう」という理由でまとめて昇格しない。
まとめ
本記事のポイントを整理すると、次のようになります。
- ClickOnce アプリ本体を「常に管理者として起動」することはできない。
- 管理者権限が必要な処理だけ、
ProcessStartInfoで PowerShell をVerb = "runas"付きで起動し、UAC の許可を得て実行する。 - 引数の渡し方は、簡易には
-File方式、より堅牢には-EncodedCommand方式を使う。 - PowerShell 側は
paramブロックで受け口を整理し、スクリプトの配置や ClickOnce のパス解決もきちんと設計する。 - 無人実行や UAC 回避が必要な場合は、タスクスケジューラや Windows サービスなど、OS の仕組みと組み合わせたアーキテクチャに切り替える。
- 管理者権限で動くコードであることを意識し、最小権限・バリデーション・ログ・署名といったセキュリティ対策を徹底する。
これらを押さえておけば、ClickOnce で配布した C# アプリからでも、安全かつ現実的に PowerShell を管理者権限で実行できるようになります。新規開発はもちろん、既存ツールのリプレースや運用改善の際の参考になれば幸いです。

コメント