🆕

GitHub に新しいリポを作って cd するまでに 3 コマンド叩く生活をやめた — ghnew を npm に公開するまで

に公開

はじめに:「新規プロジェクト始めるか」のたびに 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 一発で新ディレクトリに移動できる。ただしユーザーが各自 ~/.zshrcsource してもらう必要がある

ここで 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 つ。

const account = await select(
  { message: 'account:', choices },
  { output: process.stderr }  // ← これがないと UI が消える
);

このあたりは将来の自分 (と 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.jsonscripts には 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-purpose agent
  • 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 で 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 の流儀に近い)
  • ccd "<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 つ

  1. 「全部自動」が常に正解とは限らないcd まで自動化を諦めたら設計が一気にシンプルになった。便利さの最後の 10% は要件次第。
  2. gh CLI も罠がある--hostname は存在しない、gh auth status --json hosts の方がロバスト。
  3. stdout/stderr 規律は文章化しておく。後から変更が入って壊れるから、CLAUDE.md に「不変条件」として残す。
  4. npm publish は 2026 では「パスキーでブラウザ往復」が普通。OTP コード入力という体験は消えつつある。
  5. AI レビューは並列で観点を散らすと取りこぼしが減る。性格が違う 3 エージェントに同じコードを見せるのは安いし速い。

ぜひ試してみてください。

npx ghnew my-new-app

Discussion