CHIERO의 치에로입니다.
일본어를 포함하는 .ps1 파일은 BOM(Byte Order Mark)이 포함된 UTF-8로 저장해야 합니다. 이는 Windows 표준 PowerShell 5.1이 BOM이 없는 UTF-8을 다른 문자 코드로 읽기 때문입니다.
BOM이 포함되어 저장된 PowerShell 스크립트 4개는 Windows Server 2025 실기 환경에서 작동했습니다. Claude Code(claude -p)를 통해 입력된 진입(入口) 스킬을 호출하는 과정까지 확인했습니다.
Mac 기반의 .sh 파일을 Windows의 PowerShell로 가져갈 때 동작하지 않게 되는 이유가 여러 가지 있습니다.
저는 회사에서 사용하는 폴더 세트(AI 사원의 진입점과 절차서 일체)의 .sh 파일 4개를 옮길 때, 문자 코드, 실행 정책(Execution Policy), 경로 작성법, 화면의 일본어 표시 네 가지에 대비했습니다.
이 중 BOM과 실행 정책은 의도적으로 실패하는 조건을 만들어 실제로 멈추는 지점을 실기 환경에서 확인하고 있습니다. 4개 모두 BOM 포함 상태로 저장한 후 첫 실행부터 작동했습니다.
본 기사에서는 다음 세 가지를 작성합니다.
- 옮길 때 대비해야 할 네 가지 사항과 그에 따른 코드(실물 발췌). 실패를 재현하여 확인한 내용과, 작성법 소개에 그치는 내용을 구분하여 서술하겠습니다.
claude -p를 통해 스킬이 실제로 호출되었는지 확인한 방법 - Codex의 Windows 대응에 대해 공식 문서를 참고했습니다.
확인된 것은 2026년 10월 26일이며, 환경은 단 한 대입니다.
Codex의 실기 확인과 Windows 10 및 11 PC에서의 확인은 완료되지 않았습니다.
확인 범위와 미확인 범위는 마지막 절에 나누어 작성했습니다.
| 항목 | 값 |
|---|
| 기계 | Windows Server 2025 Standard의 VPS (빌드 26100 · x64 · 로케일 ja-JP · 코드 페이지 932) |
| ... | |
| Claude Code의 공식 시스템 요구 사항은 메모리 4 GB 이상입니다. 이 기계는 3 GB로, 아래와 같은 짧은 비대화형 실행은 작동했지만, 추천할 수 있는 환경은 아닙니다. | |
Codex는 설치되어 있지 않습니다.
옮긴 파일은 다음 4개입니다.
Mac (.sh) | Windows (.ps1) | 역할 |
|---|
install.sh | install.ps1 | 진입 스킬을 Claude Code와 Codex의 스킬 저장소에 넣기 |
check_env.sh | check_env.ps1 | 버전 · 메모리 · 여유 공간 · git 등을 표시 (아무것도 작성하지 않음) |
memory-check.sh | memory-check.ps1 | 일지 누락과 링크 끊김을 계산하기 |
new-role.sh | new-role.ps1 | 새로운 담당의 초안 만들기 |
실기에서는 4개 파일 실행, install.ps1의 각 지정(指定), 실행 정책의 3가지 경우, Claude Code로부터의 호출을 포함하여 총 24 항목에 걸쳐 테스트를 진행했습니다.
.ps1 내부 내용은 .sh가 sed나 awk로 하던 처리를 PowerShell 작성법으로 대체한 것입니다 (문자열 치환이나 날짜 계산은 .NET 함수를 사용했습니다).
동작 확인은 Mac의 .sh 결과와 비교하는 형태로 진행했습니다. 비교 대상은 memory-check, new-role, install 3개였으며, check_env는 Windows 측 출력을 읽어 확인했습니다.
문자 코드, 실행 정책, 경로, 화면 일본어 표시 순서로 작성합니다.
BOM과 실행 정책은 실패를 재현하여 확인한 내용입니다. 경로는 대비된 작성법 소개이며, 화면의 일본어 표시는 대비된 작성법 소개입니다.
Mac 에디터에서 저장한 .ps1 파일은 보통 BOM이 없는 UTF-8 형태가 됩니다.
이를 Windows PowerShell 5.1로 실행하면, 일본어 문자열을 포함하는 줄이 손상됩니다.
Microsoft의 공식 문서에는 다음 취지가 적혀 있습니다.
- 비(非) ASCII 문자를 스크립트에 작성할 때는 UTF-8 BOM으로 저장해야 함
- BOM이 없으면 Windows PowerShell은 스크립트를 구식인 'ANSI' 코드 페이지 문자 코드로 읽음
이 기계의 ANSI는 일본어 CP932입니다.
같은 4개의 스크립트를 일부러 BOM이 없는 UTF-8로 다시 저장하여 실행했을 때, 구문 오류(Syntax Error)가 발생하며 작동하지 않았습니다.
')' が存在しません 와 같은 에러 메시지가 다수 출력되었습니다.
BOM을 포함하도록 되돌리자 처음부터 정상적으로 작동했습니다.
저의 세트에서는 .ps1 파일을 BOM이 포함된 상태로 저장하여 출하 전 검사에도 사용했습니다.
if ext == '.ps1':
if not raw.startswith(b'\xef\xbb\xbf'):
add('ps1 が BOM 付き UTF-8 でない', rel, 0)
Mac 측에서 빠르게 확인하려면 파일의 시작 3바이트를 보면 됩니다.
head -c 3 install/install.ps1 | xxd
# 00000000: efbb bf
efbb bf가 보이면 BOM이 포함된 것입니다.
반대로, 스크립트가 출력하는 파일은 BOM 없이 처리했습니다.
install.ps1 이 들어가는 SKILL.md 가 그 예입니다 (저의 세트에서는 .ps1 외에 BOM이 붙어 있는 경우를 지적합니다).
Windows PowerShell 5.1의 Set-Content -Encoding UTF8나 Out-File은 BOM을 추가하거나 UTF-16으로 작성하는 경향이 있습니다 (상단의 공식 문서를 보면 명령어별 기본값이 명시되어 있습니다).
그래서 저는 .NET의 WriteAllText를 사용하며, BOM이 없는 UTF8Encoding을 전달했습니다.
$Utf8NoBom = New-Object System.Text.UTF8Encoding($false)
$text = [System.IO.File]::ReadAllText($p.Src, $Utf8NoBom)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
...
읽을 때도 파일 시작 부분에 BOM이 남아 있다면 제거합니다.
실제 장비에서 사용한 48건 중, 테스트로 추가한 1건을 제외한 47건의 SKILL.md 파일을 Mac으로 가져와서, install.sh가 들어가는 파일과 비교해 본 결과, 내용은 동일했습니다 (달랐던 부분은 세 번째 절에 작성된 경로 표기만었습니다).
파일 시작 부분에는 BOM이 없었습니다.
다시 저장한 후 구문 오류가 없는지 확인하려면, Windows 측에서 Parser::ParseFile을 사용하면 실행하지 않고도 확인할 수 있습니다. 제가 실제 장비에서 사용한 API는 이것입니다. 작성 예시를 남깁니다.
$tokens = $null; $errors = $null
[System.Management.Automation.Language.Parser]::ParseFile($path, [ref]$tokens, [ref]$errors) | Out-Null
$errors.Count # 0이면 구문 오류 없음
실제 장비에서는 모두 0건이었습니다 (BOM 포함 상태에서).
두 번째는 실행 정책(Execution Policy)입니다.
저의 기계는 RemoteSigned로 설정되어 있었습니다.
공식 문서에 따르면, 어느 범위에도 설정이 없을 때의 기본값은 Windows 클라이언트에서는 Restricted, Windows Server에서는 RemoteSigned입니다.
즉, 저의 기계는 작동하기 쉬운 쪽의 기본값이었습니다.
손에 있는 Windows 10 및 11 PC에서는 같은 결과가 나오지 않을 수 있습니다.
실제 장비에서 세 가지 경우를 시도했습니다.
| 실행 방식 | 결과 |
|---|
-ExecutionPolicy를 지정하지 않음 (기계는 RemoteSigned, scp로 전송) | 작동함 |
-ExecutionPolicy Bypass를 지정 | 작동함 |
-ExecutionPolicy Restricted를 지정 | “이 시스템에서는 스크립트 실행이 비활성화되어 있어…” 메시지와 함께 중단됨 |
그래서 매뉴얼에 작성할 실행 형태는 하나로 결정했습니다.
powershell -NoProfile -ExecutionPolicy Bypass -File .\install\install.ps1 -DryRun
-ExecutionPolicy Bypass
은 이 실행 한 번에만 적용됩니다. PC의 설정은 바뀌지 않습니다 -
-NoProfile
는, 사용자의 프로필이 실행에 섞이는 것을 방지합니다 - -DryRun
는, 넣을 위치만 표시할 뿐 아무것도 쓰지 않습니다. 처음에는 반드시 이것으로 합니다.
공식 문서에는 그룹 정책(Group Policy)으로 결정된 실행 정책은 이 지정으로는 덮어쓸 수 없다고도 쓰여 있습니다.
회사 컴퓨터에서 막힌 경우라면, 먼저 정보 시스템 담당자에게 문의하는 것이 안전합니다.
인터넷에서 받은 zip 파일 안의 .ps1 은 다른 이유로 막힐 수 있습니다.
브라우저로 다운로드한 파일에는 "인터넷에서 온" 표시가 붙으며, RemoteSigned 정책에서는 서명되지 않은 것이 실행되지 않습니다.
이 표시는 Unblock-File 로 제거할 수 있습니다.
Get-ChildItem -Recurse . | Unblock-File
공식 문서에는 curl.exe 나 Invoke-WebRequest 로 받은 파일에는 표시가 붙지 않을 수도 있다고도 쓰여 있습니다.
제 실제 기기 테스트에서는 scp로 전송했기 때문에 표시가 없었습니다. 브라우저에서 다운로드한 zip을 압축 해제 직후의 실행은 아직 시도하지 않았습니다.
세 번째는 경로(Path)입니다. Mac 기반의 절차서에는 ~/.claude/skills 와 같은 표기법이 나옵니다. Windows에서는 ~/ 가 %USERPROFILE% 로 읽어집니다. PowerShell 내부에서는 $env:USERPROFILE\ 입니다.
install.ps1 은 홈 폴더를 다음과 같이 결정합니다.
$HomeDir = $env:USERPROFILE
if ([string]::IsNullOrEmpty($HomeDir)) {
$HomeDir = [Environment]::GetFolderPath('UserProfile')
...
}
넣을 위치는 -Project 로 폴더를 지정하면, 해당 폴더의 .claude\skills 이 됩니다. 지정하지 않으면, 홈 디렉토리의 .claude\skills 입니다.
실제 기기 테스트에서는 VPS의 홈에 있는 Claude Code 설정을 변경하지 않도록, 모두 -Project 를 사용하여 테스트용 폴더 안에 넣었습니다.
function Get-SkillsRoot([string]$Side) {
if ($Side -eq 'claude') { return (Join-Path $Base '.claude\skills') }
if ($CodexLegacy) { return (Join-Path $Base '.codex\skills') }
...
}
폴더나 파일의 존재 확인, 해결(Resolve), 나열(List)에는 -LiteralPath 를 사용합니다.
-LiteralPath 는 문자를 입력한 대로 사용하며 와일드카드로 해석하지 않습니다 ( [ 나 ] 를 포함하는 폴더 이름도 그대로 처리할 수 있습니다).
그리고, 사소하지만 중요한 한 줄이 있습니다. 진입 스킬(Entry Skill) 내부의 {WORKSPACE} (일련의 위치)를 실제 경로로 대체할 때, \ 를 / 로 통일하고 있습니다.
$WS = (Resolve-Path -LiteralPath $Workspace).ProviderPath
$WS = $WS.TrimEnd('\')
# ...
...
생성된 SKILL.md 에는 C:/Users/<사용자 이름>/ai-team-test/ai-team/routing.md 와 같은 형태로 경로가 쓰입니다. 절차서 내부의 다른 경로들이 / 구분이기 때문에 통일했습니다.
Claude Code는 다음 절 확인 과정에서 이 / 형태의 경로를 Grep 할 수 있었습니다. 다른 경우에는, \ 형태의 경로로 Read 했습니다. \ 그대로 작성한 경우와의 비교는 하지 않았습니다.
네 번째는 화면에 표시되는 일본어입니다. 이 기계의 콘솔은 코드 페이지 932 입니다. 네 개 모두, 시작 부분 근처에 동일한 두 줄을 배치했습니다.
일본어 출력이 깨지지 않도록 출력을 UTF-8로 통일합니다 (Windows PowerShell 5.1에서도 작동하는 방법)
try { chcp 65001 | Out-Null } catch { }
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch { }
둘 다 실패하더라도 다음 단계로 진행할 수 있도록 try 구문으로 감싸져 있습니다.
이 두 줄을 포함한 상태로 실행했을 때도, SSH를 경유하여 실행했음에도 불구하고 일본어 출력이 읽을 수 있는 형태로 반환되었습니다.
이 두 줄을 제외한 경우와의 비교는 진행하지 않았습니다.
check_env.ps1
의 출력은 다음과 같았습니다 (실제 장치의 출력 발췌입니다).
[OK] Windows 버전 Microsoft Windows Server 2025 Standard (빌드 26100)
[요확인] 메모리 3 GB (4GB 미만은, 이 일련의 동작 기준을 밑돕니다.…)
[참고] PowerShell 5.1.26100.32995 (이 창에서 작동하는 버전)
...
메모리가 '요확인'으로 표시되는 것은 이 장치의 실제 값(3 GB)에 대한 올바른 출력입니다.
Mac의 .sh와 동일한 결과를 만들기 위해 추가된 세부 사항이 두 가지 있습니다 (제외했을 경우와의 비교는 진행하지 않았습니다).
첫 번째는 줄바꿈 문자입니다.
new-role.ps1은 템플릿 문자열에 자리 표시자(placeholder)를 넣고, 이를 치환한 다음, 끝에 LF 줄바꿈을 추가하여 작성합니다.
function Fill([string]$t) {
return $t.Replace('@@NAME@@', $RoleName).Replace('@@ID@@', $Id).Replace('@@SKILL@@', $SkillName)
}
...
생성된 세 개의 파일을 Mac의 new-role.sh가 만든 결과물과 diff -r로 비교했을 때 차이가 없었습니다.
두 번째는 날짜 계산입니다.
memory-check.ps1은 일지 파일명의 날짜를 문화권(Culture)을 고정하여 읽습니다.
$Culture = [System.Globalization.CultureInfo]::InvariantCulture
$FirstDay = [datetime]::ParseExact($First, 'yyyy-MM-dd', $Culture)
일지(logbook)를 3권 두고 실행했을 때, '일지 누락' 섹션의 출력이 Mac의 memory-check.sh와 한 글자도 틀림없이 같았습니다 (최신이 3일 전 / 평일 공백 6일).
여기부터가 Claude Code 확인입니다.
진입 스킬을 넣고, 같은 폴더에서 claude -p (비대화형)를 실행했습니다.
cd C: extbackslash Users<사용자명> extbackslash ai-team-test extbackslash ai-team
powershell -NoProfile -ExecutionPolicy Bypass -File . extbackslash install extbackslash install.ps1 -DryRun -Project . -Claude
powershell -NoProfile -ExecutionPolicy Bypass -File . extbackslash install extbackslash install.ps1 -Project . -Claude
...
실제 실행에서는 -Project에 폴더의 절대 경로를 전달했습니다. 결과는 동일합니다.
진입 스킬의 이름은 aiteam입니다. 이 실행 전에 Claude Code에 로그인해 두었습니다.
결과는 종료 코드 0이었으며, 24개 담당자의 이름이 표로 반환되었습니다 (출력의 첫 번째 줄입니다. 나머지는 담당 ID와 역할의 표였습니다).
The routing.md file lists 24 roles. Here are their names (呼び出し語 / 担当ID):
다만, 첫 번째 질문은 routing.md를 명시하고 있습니다. 작업 폴더에 실제 파일이 존재하므로, 스킬이 호출되지 않아도 파일을 읽으면 답을 얻을 수 있습니다.
그래서 두 번째는 routing.md를 명시하지 않는 질문으로 하여, 호출 기록 자체를 확인했습니다.
claude -p "aiteam を使って、LP を作りたい時はどの担当に振り分けるか、担当名だけ答えて。" --max-turns 4 --output-format stream-json --verbose
공식 문서에 따르면, stream-json은 --verbose와 함께 사용합니다.
출력은 한 줄마다 하나의 이벤트 JSON입니다.
두 번째 기록에서 확인한 내용은 다음과 같습니다.
Skill 도구 호출(tool_use)이 있으며, aiteam을 호출하고 - 그 후에 일련의 과정 중 routing.md를 C:/Users/<사용자명>/ai-team-test/ai-team/routing.md 형태의 경로로 Grep하는 과정을 거쳤습니다.
- 총 4턴 만에 성공했으며, 담당 이름이 하나 반환되었습니다.
첫 번째 기록에서도 stream-json으로 기록을 확인했습니다. 시작 시점의 스킬 목록에 aiteam이 있었고, routing.md를 Read하고 있었습니다(2턴).
확인 과정에서 주의해야 할 점 세 가지가 있습니다.
--bare 옵션을 붙이지 않습니다. 공식 문서에 따르면, --bare는 스킬의 자동 발견을 생략하기 때문에 스킬 확인에는 사용할 수 없습니다.
- 답변 문구 외에
tool_use를 확인합니다.
- 질문에는 스킬 이름(
aiteam)만 넣고, 읽어주기를 원하는 파일 이름은 넣지 않습니다.
수정한 부분은 제가 SSH를 통해 명령어를 조합할 때 발생한 따옴표 중첩 오류뿐이었습니다. 일본어 등을 포함하는 따옴표 문제나 문자 깨짐 현상은 claude -p 호출에서는 나타나지 않았습니다. 쓰기 작업(write operation)을 수반하는 절차는 이번 확인 과정에서 시도하지 않았습니다.
공식 문서에 따르면, claude -p는 승인이 필요한 작업을 허가 지정이 없으면 거부합니다. 쓰기를 수반하는 절차를 비대화형으로 실행하려면 --allowedTools 또는 허용 모드 지정이 필요합니다.
실제 기기에서 확인한 범위를 벗어나는 내용은 공식 문서에 명시된 내용만 출처 URL과 함께 작성합니다.
공식 설정 페이지에는 다음 내용이 적혀 있습니다.
- 지원 환경은 Windows 10 1809 이상 및 Windows Server 2019 이상입니다. 메모리는 4 GB 이상이어야 합니다.
- 셸(Shell)은 PowerShell과 CMD를 지원합니다. Git for Windows는 선택 사항이며, 없을 경우 PowerShell을 셸 도구로 사용합니다.
- PowerShell의 시작 부분은
PS C:\이고, CMD는 C:\입니다. PowerShell에 CMD용 &&를 붙이면 구문 오류가 발생하고, CMD에 irm을 붙이면 인식되지 않습니다. WSL도 사용할 수 있습니다.
출처: https://code.claude.com/docs/en/setup
비대화형 실행(-p): https://code.claude.com/docs/en/headless
이 절과 이전 절의 --bare 및 승인에 대한 설명은 제가 10월 27일에 읽은 공식 문서 내용입니다. 실제 기기의 Claude Code는 2.1.198 버전이며, 문서에는 더 최신 버전의 내용이 포함되어 있습니다.
제 장비에는 Git for Windows가 설치되어 있습니다. Git이 없는 환경은 테스트하지 않았습니다.
Codex에 대해서는 실제 기기에서 확인하지 않았습니다. 공식 문서에 적힌 내용만 작성합니다.
- Codex는 PowerShell을 사용하여 Windows의 샌드박스 내에서 네이티브로 구동됩니다. WSL이나 가상 머신은 필수 사항이 아닙니다.
- Windows 11이 권장됩니다. Windows 10은 업데이트된 최신 버전(1809 이상)에 한해서 '가능한 범위'의 취급을 합니다.
- Linux용 도구가 필요할 경우 등에는 WSL을 사용하는 선택지도 있습니다.
- 사용자 스킬 경로는
$HOME/.agents/skills이며, 리포지토리 스킬 경로는 작업 폴더부터 리포지토리 루트까지의 .agents/skills입니다. .codex/skills는 포함되어 있지 않습니다.
출처: https://learn.chatgpt.com/docs/windows/windows-sandbox
출처: https://learn.chatgpt.com/docs/build-skills
읽은 범위 내에서는 $HOME이 Windows에서 무엇을 의미하는지에 대해서는 명시되어 있지 않습니다.
일반적으로 %USERPROFILE%입니다.
읽을 수 있지만, 확인하지 못했습니다.
따라서, install.ps1 -Codex는 %USERPROFILE%\.agents\skills\aiteam\SKILL.md에 넣고, 기존의 .codex\skills에 넣고 싶은 사람들을 위해 -CodexLegacy를 준비했습니다.
실제 장비에서 확인한 것은, -Project로 폴더를 지정했을 때 해당 폴더의 .agents\skills와 .codex\skills 아래에 파일이 생성되는 지점까지입니다.
홈 디렉터리(%USERPROFILE%)로의 실제 쓰기 작업과, Codex가 이를 읽을 수 있는지는 확인하지 못했습니다.
확인한 것 (Windows Server 2025・PowerShell 5.1・일본어 환경의 1대)
.ps1 4개가 작동합니다. memory-check, new-role, install의 3개는 Mac의 .sh 결과와 일치합니다.
-ExecutionPolicy를 지정하지 않은 실행과 Bypass 실행이 작동합니다. Restricted에서는 멈춥니다. BOM 없이 다시 저장하면 구문 오류로 인해 작동하지 않습니다.
install.ps1의 -DryRun, 삽입, 이미 존재할 때 멈춤, -Force를 통한 덮어쓰기.
install.ps1 -Project로 삽입된 진입 스킬이 해당 폴더의 claude -p에서 호출되어, 일체(一式) 내의 파일을 읽습니다.
확인하지 못한 것
- Codex를 Windows에서 실행하여 진입 스킬이 읽히는지 여부
- Windows 10/11 PC, PowerShell 7 (
pwsh), 일본어 외 로케일 - 브라우저에서 다운로드한 zip을 압축 해제 직후 실행.
- 홈 디렉터리의
%USERPROFILE%\.claude\skills와 %USERPROFILE%\.agents\skills로의 실제 쓰기, 대화 화면, 각 담당 스킬로부터의 호출 - Git for Windows가 설치되지 않은 환경.
- 담당 업무 내용(아침 보고서 제출 등)의 실행.
테스트가 끝난 후, 테스트용 폴더와 zip은 삭제했습니다(삭제 후 확인을 통해 폴더가 없는 것을 확인했습니다).
옮기기 전에 확인할 4가지
.ps1은 BOM이 포함된 UTF-8로 저장합니다(head -c 3 file | xxd에서 efbb bf를 확인). 실행은 -NoProfile -ExecutionPolicy Bypass -File로 작성하고, 처음에는 -DryRun.
- 경로는
$env:USERPROFILE와 -LiteralPath. 스킬에 작성하는 경로는 구분자를 하나로 통일합니다(저는 /). 스킬이 호출되었는지는 stream-json의 tool_use에서 확인합니다.
- AI 사원 일체 (note): AI 사원을 만들 수 있는 것은 이것입니다. 24개의 담당과 회사 규칙을 139개의 파일로 전달하는 일체(一式)입니다. Mac과 Windows의 PowerShell에서 사용할 수 있습니다(확인한 범위는 위 절과 같습니다).