✨ はじめに
こんにちは!
わたしは、機能実装や修正はVS Codeの拡張機能のClaude Code、日々のワークフローはWindowsアプリのClaude Coworkと使い分けています。
今回は、Claude の Agent Skills を業務ワークフローに組み込んでCoworkで運用し始めたときに、実際に踏み抜いた「運用のハマりどころ」を共有します。
Skills は便利なんですよ。SKILL.md を1枚書いておけば、毎回同じ指示を貼り直さなくても Claude が勝手に「あ、これはあのスキルの出番だな」と判断して動いてくれる。
…のですが、実際にワークフロー(朝のメール/Slackチェック、週報生成、Backlogの停滞課題抽出みたいなやつ)を組んで毎日回し始めると、ドキュメントだけ読んでてもピンと来ない罠にいくつか踏み抜きます。
この記事は、その「踏み抜いた跡」の記録です。Qiita 操作用のカスタムスキルを Claude Cowork(Windows のデスクトップ版) で自作・登録したときに、実際に出たエラーメッセージと対処をそのまま載せています。同じ穴に落ちる人が減れば、というのと、自分用のメモを兼ねて。
対象読者: claude.ai / Claude Cowork / Claude Code でカスタム Skill を自作・運用しようとしている人
🖥️ 検証環境・前提
- 環境: Claude Cowork(Windows 版デスクトップアプリ)
- スキル構成: Python スクリプト + 外部 API(Qiita API v2)を叩く、いわゆる「外部連携系」スキル
- 認証: アクセストークンをローカルの
.envに保存して利用
「対話で完結する単発スキル」ではなく、スクリプト実行・外部通信・認証情報の管理が絡む実務系スキルを Cowork に載せると、以下の罠に当たりやすいです。
🧩 前提:そもそも Skill は surface ごとに「別物」
最初にここでつまずきました。
Claude のカスタム Skill は、
- claude.ai(Web/アプリのチャット)
- Claude Cowork(デスクトップのエージェント)
- Claude Code(CLI / VS Code 拡張)
の3か所(+ API)で使えるんですが、この3つは見た目が地続きでも、Skill の保管場所も挙動も完全に別系統です。公式ドキュメントにもはっきり「カスタム Skill は surface 間で同期しない」と書かれています。
Skills uploaded to claude.ai must be separately uploaded to the API. Claude Code Skills are filesystem-based and separate from both.
(claude.ai にアップした Skill は API に別途アップが必要。Claude Code の Skill はどちらとも分離している)
— Agent Skills - Claude Docs
つまり「Claude Code で作ったスキルが claude.ai のチャットにも出てくるだろう」と期待すると、出てきません。それぞれに登録する必要がある。デスクトップアプリの中で chat / Cowork / Code がタブで並んでいるので余計に「同じだろう」と錯覚しますが、中身は別物だと思っておくと事故りません。
そして本題はここから。「別物」なのは保管場所だけではなく、登録の正規ルートも、スクリプトの実行環境(どの OS で動くか) も、固有情報の置き場も、ぜんぶ surface 固有の事情を抱えています。以下、Cowork で実際に踏んだ順に3つ紹介します。
🪤 ハマりどころ① 「登録」は .skill アップロードが唯一の正規ルート
これが一番ハマりました。しかも厄介なのは、一見うまくいったように見えて、再起動すると消える ところです。
スキル本体を組んだあと、「フォルダに置けば認識されるだろう」と思って、スキルのレジストリらしき manifest.json を見つけ、スクリプトでエントリを直接追記しました。その場では Customize にスキルが出る。やった、と思って Cowork(と念のため PC)を再起動する。
Customize > スキルに、さっき追加したスキルが表示されない。
調べてわかったのは、manifest.json への直接編集は Cowork 起動時のサーバー同期で巻き戻されるということ。ローカルのファイルをいくらいじっても、クラウド側の登録状態が「正」なので、起動のたびに上書きされて消えます。
⚠️ ローカルファイル(
skills/フォルダへの直置き、manifest.jsonの直編集)でのスキル登録は 再起動で全部巻き戻ります。一時的に動いて見えるので一番タチが悪いです。
✅ 正しい登録フロー(Cowork)
唯一安定したのは、.skill パッケージを作って UI からアップロードするルートでした。
- 既存スキル(
backlogなど)を雛形に、<skill名>/配下へSKILL.md/scripts//references//.claude-plugin/plugin.jsonを配置- ⚠️ フォルダ名は最終スキル名と同じにする。
_skill/のような作業用フォルダ名のまま zip すると、.skill内が_skill/SKILL.mdになって別名スキル扱いされる
- ⚠️ フォルダ名は最終スキル名と同じにする。
-
SKILL.mdの YAML frontmatter を検証(nameは kebab-case、descriptionは 1024 字以内、<>を入れない) -
.skill(実体は zip)にパッケージング -
Customize > スキル > +ボタン > スキルを作成 > スキルをアップロード で、開いた POPUP に
.skillをドラッグ&ドロップ
これで「個人のスキル」として、サーバー側にも紐付いた形で登録されます。
⚠️ ついでに踏んだ周辺の罠
- スキルの実体は2か所ある。 編集用の「ソース置き場」(自分の repos 配下など)と、アップロード結果としてアプリ内部にできる「実行時に読まれる実体」は別物です。後者はアプリ管理の読み取り専用で、ソースをいくら直しても実行時版は変わりません。直す → 再パッケージ → 再アップロードまでがワンセットです。
- 同名スキルの再アップロードは、旧版を先に削除。 上書きだと manifest 残留や skillId 重複で不整合を起こすことがあります。Customize の該当エントリ → 削除 → 新版アップロードが確実。
- 発火確認は必ず新しいセッションで。 チャットが認識しているスキル一覧はセッション開始時に固定されるので、登録した直後のセッションには出てきません。新規チャットを開いて「{スキル名}で{基本動作}して」を試す。
-
present_filesで.skillをチャットに出そうとすると失敗しがち。 Cowork は UWP/MSIX パッケージなのでパス解決が食い違い "not accessible" になることがあります。素直にフルパスをテキストで受け取って、エクスプローラから手動 D&D が早いです。
ドキュメントには「zip にして UI からアップロード」と書いてあるんですが、「ローカル直編集は無駄(むしろ同期で消える)」 という負の知識は、自分で manifest をいじって再起動するまで実感できませんでした。
🪤 ハマりどころ② 実行環境は「Windowsデスクトップ」じゃなく「Linux サンドボックス」
スキルが Python スクリプトや bash を実行するとき、それが動いているのは Windows ではなく、隔離された Linux サンドボックスです。ここを Windows 前提で書くと、ローカルで通ったはずのスキルが新セッションでコケます。
実際に出たのがこれ。
Qiitaスキルの実行を試みましたが、APIトークンが現在の実行環境
(Linuxサンドボックス)から読み取れない状況です。
原因は、スクリプトが Path.home() / ".env" を読みに行っていたこと。Cowork の bash 内では Path.home() は /sessions/<id>/ を返すので、Windows ホームに置いた C:\Users\<user>\.env は物理的に見えないんです。
一方で、Cowork で接続(選択)したフォルダは、Linux 側に /sessions/<id>/mnt/<フォルダ名>/ としてマウントされて読めます。なので対処はこうなりました。
- トークンの
.envを Windows ホームと、接続フォルダの両方に書く - 読み込み側は Linux なら
/sessions/*/mnt/*/.envを最優先で探し、無ければ~/.envにフォールバックする探索ロジックを実装する
def candidate_env_paths():
paths = []
if sys.platform.startswith("linux"):
for env_file in Path("/sessions").glob("*/mnt/*/.env"):
if env_file.is_file():
paths.append(env_file)
paths.append(Path.home() / ".env") # Windows ネイティブ実行時のフォールバック
return paths
🌀 WSL の UNC パスは渡せない(しかもセッションごと巻き込んで詰む)
バックアップ先に WSL 内のパス \\wsl.localhost\Ubuntu\home\... を接続しようとしたら、フォルダ接続が
UNC paths are not supported
で即拒否。さらにマズかったのが、セッション開始時の選択フォルダが UNC パスのままだと、Cowork が裏でマウントを試行し続けて失敗し、その失敗状態が bash ツールまで丸ごとロックすること。ls や echo test のような単純なコマンドすら同じ UNC エラーで弾かれて、そのセッションは復旧不能になりました。
⚠️ WSL の
\\wsl.localhost\...系パスは接続できません。しかも選択フォルダを UNC のままにしておくと、bash ツール自体が使えなくなってセッションごと詰みます。最初から Windows ネイティブパス(C:\Users\...)で運用するのが安全。詰んだら新セッションで選択フォルダを切り替えて引き継ぐしかないです。
📁 outputs は普通のファイルシステムじゃない(FUSE 制約)
仕上げで地味に効いたのがこれ。Cowork の outputs(/sessions/<id>/mnt/outputs/)は FUSE マウントで、ファイル差し替え系の操作が一部禁止されています。
| 操作 | 可否 |
|---|---|
新規作成(cat > new, cp) |
✅ |
追記(>>)、mv -f での rename 上書き |
✅ |
既存ファイルの rm
|
❌ Operation not permitted
|
既存ファイルの > file 切り詰め上書き(O_TRUNC) |
❌ Permission denied
|
なので「既存ファイルを丸ごと差し替える」ときは、rm や > ではなく .new に書き出して mv -f で確定する方式に寄せる必要があります。zip も出力先が既存だと最終 rename に失敗してランダム名の temp に残るので、mv -f <temp> xxx.skill で確定させるのがコツでした。
🪟 おまけ:Windows シェル・改行・仮想化の小ネタ
Linux サンドボックスとは別に、Windows 側で .bat やパスを扱うときにもいくつか踏みました。
-
PowerShell でカッコ入りパスが壊れる。 ユーザー名に
(Tribeck)のようなカッコが入っていると、PowerShell がサブ式と解釈してTribeck:用語...は認識されませんになります。.bat起動は cmd.exe で"..."引用が無難(PowerShell なら& "...")。 -
.batは CRLF 必須・先頭にchcp 65001。 Linux 側で作った.batは LF 改行になりがちで、cmd.exe で実行すると各行が分断されて'n' は、内部コマンドまたは…として認識されていませんみたいな謎エラーになります。CRLF に変換し、日本語 echo を使うなら1行目にchcp 65001 >nul 2>&1を。 -
UWP のファイル仮想化。 Cowork は MSIX/UWP パッケージなので、ツールが書き込んだ実体パスとユーザーが見るパスが食い違うことがあります。スキル一式は仮想化の影響を受けにくいユーザー作業フォルダ配下に置き、重要な書き込み後は
dirで目視確認すると安心です。
🪤 ハマりどころ③ 認証情報と「ユーザー固有情報」の置き場所が悩ましい
スキルを動かす・共有するうえで、API キーや個人パスをどこに置くかが最後まで悩みどころでした。
🔑 大前提:トークンをチャットに貼ってはいけない
最初、何も考えずアクセストークンをチャットに貼り付けたら、Claude に 即「漏洩扱いなので無効化してください」 と止められました。チャット履歴は保存されるので、API キーはチャットを経由させず、ローカルの .env に直接保存するのが設計の基本です(特に write 権限付きトークンは流出リスクが大きい)。再発行して、ローカル入力用の .bat 経由で .env に入れ直しました。
🐛 .env の BOM で python-dotenv が無言で壊れる
.env にトークンの行はあるし、ファイルサイズも 64 byte ある。なのに os.getenv("QIITA_ACCESS_TOKEN") が空文字を返す。これでしばらく悩みました。
犯人はファイル先頭の UTF-8 BOM(EF BB BF)。load_dotenv のデフォルトは encoding="utf-8" なので BOM を読み飛ばさず、最初の変数名が QIITA_ACCESS_TOKEN(先頭に不可視の BOM 付き)として登録されてしまい、getenv でヒットしなくなっていました。
:: 先頭4バイトを確認。efbbbf で始まったら BOM 付き
python -c "from pathlib import Path; print((Path.home()/'.env').read_bytes()[:4].hex())"
対処は、書く側を BOM 無し UTF-8 で出力し、読む側は古いファイルにも耐えるよう load_dotenv(p, encoding="utf-8-sig") を明示する、の両面でした。
🤝 共有するなら「固有情報は CLAUDE.md に分離」。ただし自動ロードは当てにしない
スキルを他人に渡せるようにするには、SKILL.md や scripts に散らばった実行者固有情報(ユーザー名・個人パス・バックアップ先)を外に出す必要があります。やったのは次の通り。
- 固有情報を
SKILL.mdと同階層のCLAUDE.mdに集約(トークンは引き続き.env) -
.skillにはCLAUDE.mdも.envも含めない(個人情報ゼロのパッケージにする) -
SKILL.md冒頭に「起動時にまずCLAUDE.mdを読む。無ければ初回実行としてプロンプトで聞いて生成する」フローを明記
ここで効いた知見が2つあります。
ひとつは、スキルのサブフォルダに置いた CLAUDE.md は自動ロードされないこと。Claude Code 的な「プロジェクト直下の CLAUDE.md は自動で読まれる」感覚で SKILL.md の隣に置いても、Cowork は勝手には読みません。SKILL.md の中で「./CLAUDE.md を読む」と明示するか、もっと確実にするなら毎回接続している作業フォルダ側の CLAUDE.md/.env を Read する設計にする必要があります。
## 0. ローカル設定を毎回必ず再読込
1. 接続済みフォルダ内に `CLAUDE.md` があるか `Read` で確認する。
無ければユーザーにフォルダ接続を依頼する(中止されたら推測で続行せず止める)。
2. `CLAUDE.md` を**毎回 `Read` で再読込**する。記憶ではなくファイルから取得。
- 必要な値が無い/壊れている場合は、何が問題かを伝えてから実行を終了する。
もうひとつは、「接続フォルダのパス」だけは .env に置けないこと。接続フォルダのパスはフォルダ接続(request_cowork_directory)に渡すブートストラップ値で、.env 自体がそのフォルダの中にあるため「.env を読む前に分かっている必要がある」鶏と卵になります。だから接続フォルダパスの正しい置き場所は、Claude が起動直後に読む CLAUDE.md 側、という整理に落ち着きました。
💡 フォルダ接続そのものはセキュリティ境界で、毎回ユーザーが許可ダイアログで承認します。Claude が勝手に
C:\配下をマウントすることはできないので、「SKILL.mdの最初のステップで接続を促す」のが定石です。
📋 chat / Cowork / Code 早見表
3つ(+ API)の違いを、Skill 運用の観点でまとめるとこうなります。
| 観点 | claude.ai(チャット) | Claude Cowork | Claude Code |
|---|---|---|---|
| カスタム Skill の登録方法 | 設定 → スキル(Customize)から zip をアップロード | Customize > スキル > + から .skill をアップロード(ローカル直置き・manifest 直編集は同期で消える) |
~/.claude/skills/(個人)or .claude/skills/(プロジェクト)に ディレクトリ配置
|
| 保存先 / スコープ | クラウド・アカウント単位(本人のみ) | ローカル PC に保存(実体はアプリ内部の読み取り専用。編集はソース→再パッケージ) | ローカル filesystem・git 管理可 |
| スキル実体の編集 | 不可(zip 再アップ) | 不可(直編集は再起動で巻き戻る/.skill 再アップが必須) |
ファイルを直接編集 |
| surface 間の同期 | なし(各 surface に個別登録) | なし | なし |
| スクリプトの実行環境 | Code Execution(隔離環境) |
Linux サンドボックス(Path.home() は /sessions/<id>/。Windows の ~ は見えない) |
ユーザーのローカル OS そのまま |
| ユーザーフォルダ接続 | — |
request_cowork_directory(毎回ユーザー承認・UNC パス不可) |
OS のファイルに直接アクセス |
| MCP / コネクタ | リモート MCP コネクタ | built-in コネクタ + リモート/ローカル MCP | ローカル含めフル |
| MCP 依存スキルの安定動作 | △〜×(実体験では動かず) | ○ | ○ |
| ネットワークアクセス | 設定次第(full/partial/none) | フル(ローカル実行) | フル |
| 固有情報・認証の置き場 | アカウント設定内 |
.env(トークン)+ 接続フォルダ/CLAUDE.md(パス等)。チャット直貼り厳禁
|
.env / 環境変数 |
| 定期実行 / トリガー | なし(対話のみ) | Scheduled Tasks | Routines / headless |
| 向いている用途 | 単発・対話的なタスク | 定型ワークフロー、サブエージェント並列、ファイル生成 | 開発、CLI/headless、自動化 |
※「MCP 依存スキルの安定動作」「実行環境」などは筆者の Cowork(Windows)での運用上の観測です。コネクタの種類やネットワーク設定、バージョンによって挙動が変わる可能性があります。
📌 まとめ:運用で効く3つの心得
-
登録は
.skillアップロードが唯一の正規ルート。 ローカル直置きやmanifest.json直編集は、その場は動いても再起動の同期で巻き戻る。「直す → 再パッケージ → 再アップロード → 新セッションで発火確認」までがワンセット。 -
スクリプトの実行環境は Linux サンドボックス。 Windows の
~/.envは見えない。.envは接続フォルダにも置き、/sessions/*/mnt/*/.envを探す。WSL の UNC パスは接続できない(セッションごと詰むので注意)。outputsは FUSE 制約でrm/上書きが不可、差し替えはmv方式。 -
認証・固有情報の置き場を分離する。 トークンはチャットに貼らず
.env(BOM に注意)。共有するなら固有情報をCLAUDE.mdに外出しして.skillから除外。ただしサブフォルダのCLAUDE.mdは自動ロードされないので、SKILL.mdで毎回読むよう明示する。
特に「ローカル直編集が同期で消える」と「実行環境が Linux サンドボックス」は、ドキュメントを眺めているだけだと実感が湧きにくく、自分で manifest をいじったり新セッションでトークンが読めなくて初めて「あー!」となるやつでした。これからカスタム Skill を業務に組み込む人の、無駄な数時間を節約できれば幸いです。