SLOPSHOPPER

nda-guard

Refuse outbound tool calls carrying AWS IDs/ARNs, local NDA terms, or gendered pronouns

newguard
A shopper browsing a rack in a slop shop
README

sakanasakanasakana

herdr + Claude Code のターミナル環境を保存・バージョン管理・別マシンへ書き出すための dotfiles。 構成は void2610/dotfiles を参考に、install.sh でシンボリックリンクを張る方式。

Personal dotfiles for herdr + Claude Code. install.sh symlinks each tracked file into $HOME, backing up anything it would overwrite to ~/backup.

これは何をするか / What it does

  • show <path> — ファイル/フォルダを herdr + Kitty 用ビューアへ自動振り分け。画像→chafa、動画/音声→mpv、markdown→glow、PDF→ページ画像、diff→色付き、CSV→整列表、JSON→jq、コード→エディタ、フォルダ→open。追加ツール無しでも動く(poppler/delta/csvkit があればより綺麗)。※新タイプ(pdf/diff/csv/json)は現状 macOS 版のみ、Linux 版は未移植。
  • notify <cmd…> — コマンド実行 → 完了時に herdr 通知(所要時間 + 終了コード、成功/失敗で音変化)。長時間タスク向け。cross-OS。
  • clip <path…> — ファイルをクリップボードへ file object として載せる(macOS=osascript / Linux=xclip)。
  • git ブランチ可視化デーモン — herdr の workspace サイドバー + 各 pane ボーダーに <repo> · <branch> · <N>Δ · PR#<n> を表示。macOS は launchd、Linux は systemd で常駐。
  • Claude ステータス監視 — <https://status.claude.com>(Statuspage、認証不要)を 60 秒毎に polling し、Claude Code の statusline に色付きドットを表示(緑=正常 / 黄=● Code degraded / 赤=● API/Code outage / 青=maintenance / 灰=stale)。状態が変化した時のみ herdr 通知 + デスクトップ通知(macOS=osascript / Linux=notify-send)。statusline 側はキャッシュ読みだけで通信しない。障害を待たずに全状態を試せる test.sh(fixture 生成・ネット不要、pin で実 statusline を任意状態に固定)付き。macOS=launchd dev.claude.statuspoll / Linux=systemd claude-status.timer。
  • herdr 本体設定 — 通知・サウンド・テーマ・キーバインド・kitty_graphics を再現。
  • Claude Code 設定 — settings.json / CLAUDE.md / hooks / commands / skills / scripts をバージョン管理。
  • 1 コマンドで新マシンへ展開 — install.sh が symlink を張り(既存は ~/backup へ退避)、OS 別サービスを登録。依存や launchd/systemd の差は Claude Code が SETUP.md を読んで吸収する。

要は「herdr + Claude Code のターミナル環境まるごと」を保存・バージョン管理し、別マシンへ再現可能にするリポジトリ。

レイアウト / Layout

OS ごとに適用範囲を分ける。各ディレクトリは $HOME をミラーする。

sakanasakanasakana/
├── install.sh          # 依存(端末stack)インストール + shared/ + <os>/ を $HOME へ symlink
├── lib/deps.sh         # 状態認識インストーラ(herdr/kitty/CLI、install.sh が source)
├── packages/           # 端末stackのパッケージ一覧: brew.txt / apt.txt / pacman.txt
├── bootstrap.sh        # 追加: Claude機能用の extras(node/python)
├── deps/               # extras マニフェスト: npm-global.txt
├── DEPENDENCIES.md     # 機能 → 依存マトリクス(何が何を要るか)
├── SETUP.md            # OS 適応の手順書(Claude Code が読む)
├── CLAUDE.md           # Claude Code 自動読込のプロジェクト指示 + ガードレール
├── .github/workflows/  # CI(shellcheck + dry-run マトリクス)
├── shared/             # 全OS共通($HOME ミラー)
│   ├── .local/bin/           show, herdr-branch-labels.sh
│   ├── .config/herdr/        config.toml
│   └── .claude/              settings.json, CLAUDE.md, hooks/, commands/, skills/, scripts/
├── macos/              # Darwin 限定
│   └── Library/LaunchAgents/ dev.herdr.branchlabels.plist.tmpl
└── linux/              # Linux 限定(herdr + Kitty 前提の上書き)
    ├── .local/bin/           show(Kitty 版), clip
    └── .config/herdr/        config.toml(Linux キーバインド)

install.sh は常に shared/ を反映し、その上に uname で判定した macos/ か linux/ を重ねる。同一リポジトリで Mac と Linux 両方をまかなう。

追跡している主な物 / What's tracked

herdr / ターミナル

パス役割
.local/bin/showshow <path> — ファイル/フォルダを自動振り分け。shared(クロスOS)は 画像→chafa -f kitty、動画→mpv --vo=kitty、md→glow。linux 上書き版は herdr + Kitty 前提: フォルダ→xdg-open、画像→kitten icat、動画→flatpak mpv --vo=kitty、md→glow、コード/テキスト→micro(pane 内エディタ, マウス+キーボード)。
linux/.local/bin/clipclip <path>… — ファイルを X クリップボードに file object(xclip -target text/uri-list)として載せ、ファイルマネージャ/チャット/メールへ添付貼り付け可能に。abs パスを file:// URI に percent-encode。X11 前提。
.local/bin/herdr-branch-labels.shgit ブランチ可視化デーモン。workspace サイドバー + 各 pane ボーダーに <repo> · <branch> · <N>Δ · PR#<n>。
.config/herdr/config.tomlherdr 本体設定(通知・サウンド・テーマ・キーバインド・kitty_graphics)。linux 上書きは prefix=alt+x・矢印 pane 移動・agent walk・cheatsheet pane。
macos/…/dev.herdr.branchlabels.plist.tmpl上記デーモンの launchd 常駐ジョブ。絶対パスが要るため symlink せず $HOME を埋めて生成。

Claude Code

パス役割
.claude/settings.jsonClaude 設定(hooks・permissions 等)。パスは $HOME でテンプレ化済み。
.claude/CLAUDE.mdグローバル指示(NDA フィルタ等)。※後述のサニタイズ済み。
.claude/hooks/herdr-claude-done.shClaude 完了 → herdr バナー + サウンド。
.claude/commands/*.mdカスタムスラッシュコマンド(standup/nippo/research 含む、サニタイズ済み)。
.claude/skills/*カスタムスキル。
.claude/scripts/*補助スクリプト(md-to-pdf, claude-status 等)。

セットアップ & インストール / Setup & Install

まず clone:

git clone git@github.com:fusuke-minagi-akari/sakanasakanasakana.git ~/sakanasakanasakana
cd ~/sakanasakanasakana

以降は 2 通り。方法 A(推奨) は OS 差(依存インストール・launchd/systemd)まで面倒を見る。

方法 A: Claude Code に任せる / Let Claude Code set it up

clone したディレクトリで Claude Code を起動し、こう頼む:

このリポジトリをこのマシンにセットアップして

Claude Code は root の CLAUDE.md を自動で読み、SETUP.md の手順(OS 判定 → install.sh[端末stack依存 + symlink + サービス] → bootstrap.sh[Claude機能extras] → herdr hook 再生成 → per-user config → 検証)に沿って適応させる。NDA ガードレール込み。

方法 B: 手動 / Manual

./install.sh            # 依存(herdr/kitty/CLI) + symlink + example 展開 + launchd 起動
./install.sh --dry-run  # 変更せず動作だけ表示
./install.sh --no-deps  # 依存インストールを飛ばして symlink だけ
./bootstrap.sh          # 追加: node/python(/diagram, report 用)

2 層構成:

  • Layer 1(端末stack) — install.sh が lib/deps.sh 経由で状態認識インストール(herdr, kitty, glow chafa mpv jq gh git file)。一覧は packages/{brew,apt,pacman}.txt。cross-OS・テスト済み — 触らない。
  • Layer 2(Claude機能extras、任意) — bootstrap.sh が Layer 1 に無い物だけ追加(node/python、--npm で npm globals)。Layer 1 は一切触らない。

機能→依存の全体は DEPENDENCIES.md。

  • 既存の実ファイルは ~/backup/<相対パス> に退避してから symlink へ置換(破壊なし)。
  • リポジトリ内を編集すれば即反映(config.toml 編集後 herdr server reload-config/スクリプト編集後 launchctl kickstart -k gui/$UID/dev.herdr.branchlabels、Linux は systemctl --user restart herdr-branch-labels)。
  • Linux: linux/.config/systemd/user/ に同梱した unit(claude-status.timer)は install.sh が daemon-reload + enable --now まで自動実行。branch-labels の unit は未同梱 — SETUP.md §2 にそのまま貼れる unit あり。

詳細な OS 別手順書は SETUP.md。

初回セットアップの手当て / Post-install

  1. herdr state hook(herdr 管理) — ~/.claude/hooks/herdr-agent-state.sh は herdr が生成・上書きするため追跡しない。再生成: ``sh herdr integration install claude ``
  2. **per-user config(*.example から seed)** — 秘匿値を含む設定は .example テンプレートのみコミット。install.sh が実ファイル不在時のみ複製する(既存は上書きしない)。中身を自分の値で埋める:
  3. ~/.claude/scripts/standup_config.json ← standup_config.example.json(Slack/Notion ID 等)
  4. ~/.claude/scripts/devices.txt ← devices.example.txt(SSH ホスト)
  5. .claude/skills/kalmia/SKILL.md の <KALMIA_NOTION_COLLECTION_ID> を実 ID に。

サニタイズ / Sanitization

NDA・PII を含む値はコミット前に汎用プレースホルダへ置換済み(クライアント企業名・AWS アカウント ID・金額・Slack/Notion ID・同僚名・LAN IP)。ルールや構造は保持、具体値のみ除去。実値は各自ローカルで補完する(*.example + .gitignore で保護)。

依存 / Dependencies

全機能→依存の完全なマトリクス(機能ごとに何が core / optional か、OS 別パッケージ名)は DEPENDENCIES.md。Layer 1 一覧は packages/*.txt(install.sh 経由)、Layer 2 extras は deps/(bootstrap.sh 経由)。

ざっくり: herdr+kitty(install.sh が自動導入)+ git jq gh perl file(daemon)、chafa mpv glow(show)、node+npx(diagram/kalmia/report)、python3(skills、stdlib のみ)、uv+pcl(show の .pcd)。daemon の hashkey は macOS md5 前提(md5 無い distro では共有キーに劣化、ラベル自体は表示)。OS 差はサービス管理(macOS=launchd 自動 / Linux=systemd user unit、SETUP.md §2)。caveman プラグイン・MCP サーバ・melchior wrapper は手動(DEPENDENCIES.md §6)。

OS 別 show/clip の追加ツール: Linux = kitten(Kitty)・flatpak io.mpv.Mpv・micro・xdg-open・xclip・realpath。macOS = chafa(icat は herdr PTY 越しで不可)・mpv・glow・${EDITOR}/micro・open・pbcopy/osascript。

追跡しないもの / Not tracked

ログ・ソケット・キャッシュ・session.json・history.jsonl・projects/・sessions/・settings.local.json・.credentials.json・実 standup_config.json/devices.txt(.gitignore 参照)。

Source 2 files
hooks/index.ts 29 lines
1import type { EngineInterface, Register } from "claude-code";
2import { findings, OUTBOUND, OUTBOUND_BASH, parseTerms, strings } from "./check";
3
4async function verdict($: EngineInterface, texts: string[]): Promise<string | undefined> {
5  const home = await $.env.get("HOME");
6  if (await $.fs.exists(`${home}/.claude/nda-guard.off`)) return undefined;
7  const terms = await $.fs.read(`${home}/.claude/nda-terms.txt`).then(parseTerms, () => []);
8  const hits = findings(texts, terms);
9  if (hits.length === 0) return undefined;
10  return `nda-guard refused this outbound call (CLAUDE.md NDA/pronoun rules): ${hits.join(", ")}. Rewrite without them (names or roles instead of pronouns; drop IDs/ARNs/client names) and retry. If this is a false positive, ask the user; they can disable the guard with: touch ~/.claude/nda-guard.off`;
11}
12
13export const register: Register = (on) => {
14  on("tool.call", { tool: OUTBOUND }, async ($, e, next) => {
15    const texts = strings(e);
16    // Artifact carries a path, not the page: scan the file it would publish
17    const path = (e as { file_path?: unknown }).file_path;
18    if (typeof path === "string") texts.push(await $.fs.read(path).catch(() => ""));
19    const deny = await verdict($, texts);
20    return deny ? { deny } : next(e);
21  });
22
23  on("tool.call", { tool: "Bash" }, async ($, e, next) => {
24    if (!OUTBOUND_BASH.test(e.command)) return next(e);
25    const deny = await verdict($, [e.command]);
26    return deny ? { deny } : next(e);
27  });
28};
29
hooks/check.ts 50 lines
1// Pure scan, no $: the validator can't follow $ across imports, so index.ts owns every $ call.
2
3// Outbound = sends, posts, or writes something another person can read.
4export const OUTBOUND =
5  /^(Artifact|mcp__claude_ai_(Slack|Notion|Gmail|Google_Drive|Google_Calendar|Claude_Docs)__.*(send|reply|forward|draft|schedule|create|update|comment|batch|share|copy|move|duplicate|upload).*)$/;
6
7// Bash commands that publish text: PR/issue bodies, gists, commit messages.
8export const OUTBOUND_BASH = /\bgh\s+(pr|issue|release|gist|api)\b|\bgit\s+commit\b/;
9
10const RULES: [string, RegExp][] = [
11  // 12 digits not inside a hex run, so UUID/Notion-id fragments don't trip it
12  ["AWS account ID", /(?<![0-9a-fA-F-])\d{12}(?![0-9a-fA-F-])/],
13  ["ARN", /\barn:aws[a-z-]*:/i],
14  ["gendered pronoun", /\b(he|she|him|his|her|hers|himself|herself)\b/i],
15  ["gendered pronoun", /彼女|彼(?!ら|岸|方)/],
16];
17
18// ids and urls are routing, not prose, and are hex-ish enough to false-positive
19const SKIP_KEY = /(id|ids|url)$/i;
20
21export function strings(v: unknown, out: string[] = [], key = ""): string[] {
22  if (SKIP_KEY.test(key)) return out;
23  if (typeof v === "string") out.push(v);
24  else if (Array.isArray(v)) for (const x of v) strings(x, out);
25  else if (v && typeof v === "object")
26    for (const [k, x] of Object.entries(v)) strings(x, out, k);
27  return out;
28}
29
30// `terms`: one per line from ~/.claude/nda-terms.txt, '#' comments; matched case-insensitively
31export function parseTerms(text: string): string[] {
32  return text
33    .split("\n")
34    .map((l) => l.trim())
35    .filter((l) => l !== "" && !l.startsWith("#"));
36}
37
38export function findings(texts: readonly string[], terms: readonly string[]): string[] {
39  const hits = new Set<string>();
40  for (const t of texts) {
41    for (const [label, re] of RULES) {
42      const m = t.match(re);
43      if (m) hits.add(`${label} "${m[0]}"`);
44    }
45    const low = t.toLowerCase();
46    for (const term of terms) if (low.includes(term.toLowerCase())) hits.add(`NDA term "${term}"`);
47  }
48  return [...hits];
49}
50