はじめに:「新規プロジェクト始めるか」のたびに 3 コマンド
新しい個人プロジェクトを始めるたびに、自分はだいたいこれをやっている。
gh repo create alice/my-new-app --private --add-readme
gh repo clone alice/my-new-app
cd my-new-app
3 コマンド。毎回。ghq 派の自分はそこに ghq get ... も挟まるので体感 4 コマンド。
「これ 1 行にできないんかな」とずっと思いつつ、~/.zshrc に関数を書きかけては「ホスト切替もしたいし、SSH/HTTPS の選択も毎回違うし…」で結局放置していた。
そんなところに npx ghnew my-new-app 一発で全部終わる CLI を作って npm に公開した。
npx ghnew my-new-app
→ repo 作成 → ghq の管理下に clone → 最後に cd "/path/to/your/new/dir" を box で表示。c を押すとクリップボードに入る。
100 行未満の薄いラッパー。なのに作る過程で詰まったところが地味に多く、Unix の根本制約や 2026 年の npm 事情にぶつかったので学びをまとめておく。
できあがったやつ
$ ghnew my-new-app
┌ ghnew
│ creating private repo on github.com…
│ ✓ created alice/my-new-app
│ cloning via https…
│ ✓ cloned
└
╭─ next ─────────────────────────────────────────────────╮
│ │
│ cd "/Users/alice/projects/github.com/alice/my-new-app"│
│ │
╰────────────────────────────────────────────────────────╯
press c to copy · any other key to exit
c を押すと cd "/Users/.../my-new-app" がクリップボードに入る。末尾改行なし、ダブルクォート付きにしてあるので、ペーストして Enter で即 cd できる。
エージェントや CI 用の非対話モードもある:
ghnew --json --remote github.com/alice my-new-app
# → {"schemaVersion":1,"host":"github.com","login":"alice", ... ,"path":"/Users/.../my-new-app"}
設計判断 1: 『子プロセスは親シェルの cwd を変えられない』
最初に詰まったのはここ。
当初は cd まで完全自動化したくて、こんなシェル関数で包む案を考えていた。
ghnew() {
local path
path=$(npx ghnew "$@") || return $?
[[ -n $path ]] && cd "$path"
}
これで ghnew foo 一発で新ディレクトリに移動できる。ただしユーザーが各自 ~/.zshrc に source してもらう必要がある。
ここで Unix の根本ルールに改めてぶつかる:
外から起動された子プロセスは、親シェルの作業ディレクトリを絶対に変えられない。
これは npx だろうが Python スクリプトだろうが Go バイナリだろうが同じ。 OS レベルで決まっている話で、回避手段は基本「親シェル自身に動かしてもらう」=シェル関数しかない。
ところが本人 (この CLI を作ってもらっていた自分) が、
「あー、cd は手動でいいよ。コピペできるくらいで十分」
と言った瞬間に設計が一気にシンプルになった。zsh 関数いらない、source のセットアップ手順不要、ただの npm パッケージ単独で完結。
便利さの最後の 10% は、要件次第で切り捨てた方がいい時がある。結果的に「c を押すとクリップボードに入る」だけの素直な CLI になって、これで十分だった。
設計判断 2: uvx / npx / zsh 関数の三択
候補は 3 つあった。
| 方式 | 起動コスト | ユーザー側準備 |
|---|---|---|
uvx (Python) |
速い (Rust 製) |
uv 必須 |
npx (Node.js) |
普通 | Node が入ってる人が多い |
| zsh 関数 | ゼロ | 各自 dotfiles に書く・自動配布できない |
「Node はもう入ってる」「@inquirer/prompts の対話 UI が成熟」「シェル関数だと配布が辛い」あたりで npx 採用。uvx も悪くなかったが、起動が一拍速い以外のメリットが薄かった。
gh CLI の落とし穴: --hostname フラグは存在しない
これで結構な時間を溶かした。GitHub Enterprise (GHE) も対象にしたかったので「gh repo create --hostname git.example.com で host 切替できるはず」と思ったら、そんなフラグは存在しない。
cli/cli#11093 で「--hostname 追加してほしい」という要望が立っている状態。正解は GH_HOST 環境変数を子プロセスに注入。
spawnSync('gh', ['repo', 'create', `${user}/${name}`, '--private', '--add-readme'], {
env: { ...process.env, GH_HOST: account.host },
stdio: ['inherit', 2, 'inherit'],
});
ついでに gh auth status も近年は --json hosts を受け付ける (gh >= 2.40)。テキストパースを捨てて JSON 一本にできる。
const { hosts } = JSON.parse(
execFileSync('gh', ['auth', 'status', '--json', 'hosts'], { encoding: 'utf8' })
);
// → { "github.com": [{ login, gitProtocol: "https" }],
// "git.example.com": [{ login, gitProtocol: "ssh" }] }
しかも gitProtocol まで取れるので、ghq get に渡す URL を SSH / HTTPS で自動切替できる。
const cloneUrl = gitProtocol === 'ssh'
? `git@${host}:${login}/${name}.git`
: `https://${host}/${login}/${name}`;
GHE は SSH、GitHub.com は HTTPS、みたいな個別設定を持っていてもユーザーが意識せずに済むのが地味に嬉しい。
stdout / stderr の規律: > out.txt で out.txt が空になる設計
スクリプト用途のことを考えて、出力規律をきっちり決めた。
-
stdout = 機械可読出力のみ:
--jsonの 1 行 JSON、--quietの path -
stderr = 人間向け全部: 進捗ログ、cd の box、
c で copyの hint、エラー
これにより ghnew foo > out.txt で:
-
out.txtは空になる (pretty モードは stdout を使わない) - 端末側には box がちゃんと表示される
-
path=$(ghnew --quiet --remote github.com/alice foo)でパスだけが取れる
ここで罠が 2 つ。
罠 1: @inquirer/prompts のデフォルト出力先は process.stdout。
シェル関数で path=$(ghnew foo) と囲まれた瞬間、プロンプト UI が stdout のパイプに吸われて画面に何も出なくなる (=ユーザーには固まったように見える)。全プロンプト呼び出しに { output: process.stderr } を明示で渡す必要がある。
const account = await select(
{ message: 'account:', choices },
{ output: process.stderr } // ← これがないと UI が消える
);
罠 2: 子プロセス (gh, ghq) も stdio: 'inherit' のままだと子の stdout が親のパイプに乗る。
['inherit', 2, 'inherit'] で fd 1 を親の stderr (fd 2) に向けないとダメ。
このあたりは将来の自分 (と AI エージェント) が壊さないよう、リポ同梱の CLAUDE.md に「不変条件」として明文化してある。
npm publish 周りの現代 (2026 年版)
久しぶりに npm に何か出して、想像以上に挙動が変わっていた。
審査は今もない
npm publish を叩いた瞬間にレジストリに反映される。App Store のような人間レビューは存在しない。
代わりに「72 時間以内なら自由に取り下げられる、それ以降は npm deprecate で塩漬けが基本」という事後対応モデル。同じ name@version は一度公開したら永久に再利用できない。
2FA が事実上必須になっていた
npm login --auth-type=web でブラウザ OAuth してログインまでは普通に通る。問題は publish 時。
$ npm publish
npm error code E_403
npm error 403 Forbidden - PUT https://registry.npmjs.org/ghnew - Two-factor
npm error authentication or granular access token with bypass 2fa enabled
npm error is required to publish packages.
npm のアカウント設定で「Require two-factor authentication and disallow tokens」を有効化しないと弾かれる。これを有効化すれば publish 時に毎回 OTP が求められて通る。
パスキー (WebAuthn) を選ぶと OTP は無くなる
2FA の方式に パスキー (WebAuthn) を選択できる。Touch ID や iCloud Keychain で認証する例のやつ。これを設定すると…
$ npm publish
npm error code EOTP
npm error This operation requires a one-time password.
npm error Open this URL in your browser to authenticate:
npm error https://www.npmjs.com/auth/cli/<random-token>
OTP コードは存在しない。代わりに表示される URL をブラウザで開く → パスキー認証 (Touch ID 等) → npm CLI 側が自動で再開して publish が完了する。
コード入力を排した代償として「ブラウザ往復が必須」になる、というトレードオフ。リモートマシンから npm publish --otp <code> で済ませる運用とは相性が悪いので、CI で publish するなら Trusted Publishing (OIDC) かトークン運用に切り替える必要がある。
Shai-Hulud worm を意識した最低限の対策
2025/09 に発生した npm の supply-chain 攻撃 Shai-Hulud は、開発者のトークンを phishing で奪取して同じメンテナの他パッケージへ自己複製感染するワーム。preinstall / postinstall script を踏み台にする。
flat name (ghnew) で個人アカウントから出す以上、せめてこれくらいは:
-
package.jsonのscriptsにはpreinstall/postinstallを絶対に置かない - README にも「
npm install --ignore-scriptsで問題なく動く」と明記 - CI で使う場合は Granular Access Token で scope を
ghnewのみに限定 - 将来的に Trusted Publishing (OIDC) へ移行
これらは CLAUDE.md (リポ同梱) で「禁止事項」として文章化しておいた。エージェントに修正させた時に postinstall 付きの形に書き換える事故を未然に防ぐ。
AI エージェント連携: .claude/skills/ghnew/SKILL.md を同梱
Claude Code 等のエージェントから安全に呼べるよう、リポに skill (SKILL.md) を同梱した。
エージェント向けのレシピをファイルに書く。
---
name: ghnew
description: Create a brand-new GitHub or GHE repo AND clone it locally...
when_to_use: |
Use when the user says "create a new repo", "新規リポ作って", ...
Do NOT use for cloning existing repos, forking, listing, deleting.
---
# Recommended call
npx -y ghnew@^0.2 --json --remote <host>/<login> <name>
# Visibility — default to private
Never pass --public unless the user explicitly says
"OSS" / "open source" / "オープンソース".
ポイントは「Do NOT use」の否定条件を明示すること。「create a new repo」だけだと既存 repo の操作 (clone / list / delete) にも誤発火するので、AND clone it locally のような追加条件で精度を上げる。
配布は skills CLI を使えばワンライナーで終わる。
npx skills add -y -g https://github.com/ryoshin0830/ghnew
repo を clone して、 SKILL.md を自動 discovery して ~/.claude/skills/ に登録してくれる。便利。
なお npm tarball には .claude/ も CLAUDE.md も含めていない (.npmignore で除外)。 skill ファイルを npm から配るとパッケージサイズが膨らむし、typosquat のリスク面が広がるので、配布は GitHub repo 経由に統一した。
3 並列でレビュー: opencode + codex + Claude general-purpose
実装が一通り終わった時点で、コードのレビューを性格が違う 3 エージェント並列でやらせた。
- Claude の
general-purposeagent -
opencode run "..."(OpenAI 系) -
codex e "..."(OpenAI codex CLI)
得意領域が地味に違うので、揃った観点もあれば独自の指摘も結構あった。
-
opencode:
--remoteの正規表現^([^/]+)\/(.+)$がスラッシュを許容して--remote github.com/alice/evilが account=alice/evilで通過する Critical バグを発見 -
codex:
--jsonモードで gh/ghq の stderr が先に出力されるから「stderr 全体を JSON として読めない」契約破れ - Claude: skill の trigger 文言が過剰反応するので否定条件 (Do NOT use) を入れろ
「3 観点で同じ指摘されたら確実、 1-2 個しか言わなかった指摘も検証する」というトリアージで動かすと、シングル AI レビューより取りこぼしが減る感覚があった。安いし速いのでオススメ。
詰まり: opencode / codex を Bash から呼ぶと無限 hang する
このレビューフローで最初詰まったのがここ。バックグラウンドで動かしたら output が一向に出てこない。
opencode run "..." 2>&1 | tail -200 &
# → 5 分待っても output file が 0 bytes
プロセスは alive、CPU は使ってない。原因を切り分けたら codex のログに:
Reading additional input from stdin...
argv で受け取ったプロンプトに加えて、stdin からも追加入力を読みに行く仕様だった。通常のターミナルなら TTY の Ctrl-D で EOF を送れるが、Bash 経由で起動すると stdin は閉じないパイプ → 永久に EOF を待つ。
</dev/null で stdin に即時 EOF を送ると解決する。
opencode run "..." </dev/null 2>&1
codex e "..." </dev/null 2>&1
シンプルな話なんだけど、ハマると気付きにくい。CLI を自動化で叩く時の典型的な罠。
おまけ: npm link と npm unlink の関係
開発中は npm link で global PATH に「シンボリックリンク」を作って、編集 → 即実行のループを回す。
cd ~/projects/.../ghnew
npm link
# どこからでも:
ghnew foo
# → ~/.../bin/ghnew → ../lib/node_modules/ghnew → ~/projects/.../ghnew
これはファイルコピーではない。シンボリックリンク。本体は手元のディレクトリそのもので、bin/ghnew.mjs を編集したら次の ghnew foo で即反映される。
publish 後に不要になったら:
npm unlink -g ghnew # ≒ npm uninstall -g ghnew
両者は実質同じコマンド。違いは「linked package を消すなら unlink」「registry インストール版を消すなら uninstall」という意図表現だけ。ローカルのプロジェクトディレクトリは無傷で残るので「unlink でソースコードが消えるんじゃ…?」みたいな心配は不要。
0.2.0 の機能まとめ
-
--json: 機械可読 1 行 JSON、schemaVersion: 1で将来の互換性判定を可能に -
--quiet: path のみ、script 用 -
--remote HOST/LOGIN:--host+--accountのショートハンド - 色つき box (Claude Code / opencode の流儀に近い)
-
cでcd "<path>"をクリップボードへ (pbcopy/wl-copy/xclip/ OSC 52 over SSH) -
NO_COLOR対応 (no-color.org 仕様準拠: env が set かつ非空のときのみ disable) - 終了コード
E_VALIDATION(1),E_AUTH(2),E_DEPS(127),E_INTERRUPTED(130) を明文化
まとめ — 学んだ 5 つ
-
「全部自動」が常に正解とは限らない。
cdまで自動化を諦めたら設計が一気にシンプルになった。便利さの最後の 10% は要件次第。 -
gh CLI も罠がある。
--hostnameは存在しない、gh auth status --json hostsの方がロバスト。 -
stdout/stderr 規律は文章化しておく。後から変更が入って壊れるから、
CLAUDE.mdに「不変条件」として残す。 - npm publish は 2026 では「パスキーでブラウザ往復」が普通。OTP コード入力という体験は消えつつある。
- AI レビューは並列で観点を散らすと取りこぼしが減る。性格が違う 3 エージェントに同じコードを見せるのは安いし速い。
ぜひ試してみてください。
npx ghnew my-new-app