Claude Media
Google Workspaceの未使用アカウントをClaude Codeで洗い出す手順

Google Workspaceの未使用アカウントをClaude Codeで洗い出す手順

Directory APIのlastLoginTimeとorg unitをCSVに落とし、Claude Codeで長期未ログインの候補と停止前の確認リストを作る手順です。

Google Workspaceの未使用アカウントをClaude Codeで洗い出す手順

Google Workspaceの未使用アカウントは、Directory APIの users.list で lastLoginTime を全ユーザー分取り、CSVにしてClaude Codeに判定させると洗い出せます。取得はスクリプトが担い、Claude Codeは基準日に沿った抽出と確認リストの作成に専念させる分担です。

ここで作るのは、削除してよいアカウントの一覧ではありません。ライセンス削減の候補と、停止や削除の前に本人や上長へ確かめる項目を並べた一覧です。

未使用の判定に使うUserリソースの項目

users.list が返すUserリソースには、棚卸しに効く項目が揃っています。

項目内容判定での使いどころ
lastLoginTime内容最後にアカウントへログインした時刻(ISO 8601)判定での使いどころ長期未ログインの基準
creationTime内容アカウントの作成時刻判定での使いどころ作成直後で未ログインのものを候補から外す
suspended内容停止中かどうか判定での使いどころ停止済みを別枠にする
archived内容アーカイブ済みかどうか判定での使いどころ別枠にする
orgUnitPath内容所属する組織部門のフルパス(最上位は /)判定での使いどころ部門ごとの集計と、確認先の振り分け
isAdmin / isDelegatedAdmin内容スーパー管理者か、委任管理者か判定での使いどころ管理者を自動で候補に入れない
isEnrolledIn2Sv内容2段階認証の登録有無判定での使いどころ参考情報として併記

lastLoginTime と creationTime は、どちらも出力専用の項目です。管理者が値を書き換えることはできません。時刻の形式は 2010-04-05T17:30:04+01:00 のようにタイムゾーン付きなので、基準日と比べるときは時刻をUTCにそろえてから比較します。

lastLoginTimeは検索クエリで絞れない

取得側で「90日以上ログインしていない人」だけに絞れれば楽ですが、users.list の query パラメータには、その手段がありません。query に使えるフィールドの一覧には name email givenName familyName isAdmin isDelegatedAdmin isSuspended isArchived orgUnitPath isEnrolledIn2Sv などが並び、lastLoginTime は含まれていません。

そのため方針は2段になります。

  1. customer=my_customer で全ユーザーを取る。必要なら query で isSuspended=false などに絞る
  2. lastLoginTime の古さは、手元で判定する

query の orgUnitPath は viewType=admin_view のときだけ使えます。たとえば orgUnitPath=/営業 のように部門を指定すると、その配下の組織部門もまとめて対象になります。また、検索結果に新しい値が反映されるまでには、通常1時間ほど、最長で36時間かかることがあります。昨日の変更が検索に出ないことがあるのは、この遅れのためです。

users.listでCSVを作る

customer には、管理コンソールの顧客IDか、自分のアカウントを指す別名 my_customer を渡します。複数ドメインを持つ組織で全ユーザーを取るときは、domain ではなく customer を使います。どちらか一方の指定は必須です。

1ページの件数は、maxResults を指定しないと100件です。続きは nextPageToken で取得します。このトークンは3日間だけ有効です。

必要な項目だけを返させる fields を付けると、個人情報の余計な持ち出しも減らせます。次は、公式の例に沿ってPythonのクライアントライブラリで書いた一例です。

pip install google-api-python-client google-auth
python list_users.py > users.csv
# list_users.py
import csv, sys
import google.auth
from googleapiclient.discovery import build
 
SCOPES = ["https://www.googleapis.com/auth/"
          "admin.directory.user.readonly"]
creds, _ = google.auth.default(scopes=SCOPES)
svc = build("admin", "directory_v1", credentials=creds)
 
FIELDS = ("nextPageToken,users(primaryEmail,lastLoginTime,"
          "creationTime,suspended,archived,orgUnitPath,"
          "isAdmin,isDelegatedAdmin)")
cols = ["primaryEmail", "lastLoginTime", "creationTime",
        "suspended", "archived", "orgUnitPath",
        "isAdmin", "isDelegatedAdmin"]
 
w = csv.writer(sys.stdout)
w.writerow(cols)
token = None
while True:
    res = svc.users().list(
        customer="my_customer",
        orderBy="email", fields=FIELDS, pageToken=token,
    ).execute()
    for u in res.get("users", []):
        w.writerow([u.get(c, "") for c in cols])
    token = res.get("nextPageToken")
    if not token:
        break

users.list の呼び出しに必要なスコープは、admin.directory.user.readonly で足ります。読み取り専用のスコープなので、このスクリプトからユーザーを停止したり削除したりはできません。取得用の認証は読み取りに絞り、停止や削除は別の接続で行うと、Claude Codeに渡すフォルダーから変更が起きる経路が残りません。

上のスクリプトは、Directory APIを読み取れる管理者権限の認証情報(サービスアカウントへのドメイン全体の委任など)が用意できている前提です。

出力は、次のような形になります。値は例示用の架空のものです。

primaryEmail,lastLoginTime,creationTime,suspended,archived,orgUnitPath,isAdmin,isDelegatedAdmin
a.tanaka@example.com,2026-10-18T01:12:44.000Z,2022-04-05T17:30:04.000Z,False,False,/営業,False,False
c.sato@example.com,2025-03-02T09:00:11.000Z,2021-06-01T00:00:00.000Z,False,False,/開発,False,False
d.new@example.com,,2026-10-01T00:00:00.000Z,False,False,/営業,False,False

3行目のように lastLoginTime が空のユーザーは、そのまま「未使用」と決めつけられません。まだ初回ログイン前の新規アカウントかもしれないためです。未ログインのアカウントに lastLoginTime がどんな値で入るかは、Userリソースのページに記載がありません。空欄でも、初期値らしい古い日時でも、「記録なし」の枠に回すルールにしておくと安全です。

Claude Codeに判定させる

CSVを置いたフォルダーに CLAUDE.md を書き、判定ルールを固定します。口頭の指示だと、実行のたびに基準がぶれるためです。

# Workspaceアカウント棚卸し
 
- 読むのは users.csv だけ。API 呼び出しやアカウント操作はしない
- 長期未ログイン: lastLoginTime が基準日の90日より前
- lastLoginTime が空欄、または 2000 年より前の値は
  「記録なし」として別枠に出す
- creationTime が基準日の30日以内なら候補から外す
- suspended か archived が True のものは別枠に出す
- isAdmin か isDelegatedAdmin が True のものは
  候補に入れず、「管理者」の枠で報告する
- 出力は primaryEmail, orgUnitPath, lastLoginTime,
  判定理由 の表。orgUnitPath ごとの件数も併記する

90日と30日は例として置いた基準です。契約の更新時期や、長期休暇を取る職種の有無に合わせて変えてください。

実行は非対話モードで行います。claude -p でプロンプトを渡すと結果が標準出力に返り、--allowedTools で許可するツールを絞れます。ここでは読み取りだけを許可します。非対話実行の詳しい作法はclaude -pの使い方にまとめています。

claude -p "users.csv を CLAUDE.md の基準で判定して。\
基準日は 2026-10-20" \
  --allowedTools "Read" > report.md

依頼文には基準日を日付で書き込みます。「90日前」とだけ書くと、Claude Codeが今日をいつと解釈したかが出力から分かりません。

行数が数千を超えるなら、読んで数えさせるより、集計スクリプトを書かせて実行させるほうが再現できます。その場合はBashを許可ツールに足し、実行できるコマンドは settings.json の権限で絞っておきます。

件数をスクリプト側の数字で検算する

出力された表は、そのまま信じず、件数をCSV側の数字と照らします。次のワンライナーで数え直せます。

python - <<'EOF'
import csv
from datetime import datetime, timezone, timedelta
cut = datetime(2026, 10, 20, tzinfo=timezone.utc) - timedelta(days=90)
n = 0
for r in csv.DictReader(open("users.csv")):
    t = r["lastLoginTime"]
    if t and datetime.fromisoformat(t.replace("Z", "+00:00")) < cut:
        n += 1
print(n)
EOF

Claude Codeが報告した「長期未ログイン」の件数と一致するかを見ます。管理者や新規アカウントを外す条件は、このワンライナーに入れていないので、差が出たら除外分で説明できるかを確かめます。説明のつかない差が残るなら、CSVの読み込み範囲や日付の解釈に問題があります。

Reports APIで別の角度から補う

Directory APIの lastLoginTime だけでなく、Reports APIのユーザー利用状況レポートからも最終ログインの時刻が取れます。accounts アプリケーションのパラメータに last_login_time があり、SSOでのログインは timestamp_last_sso という別の項目です。

GET https://admin.googleapis.com/admin/reports/v1/usage/users/all/dates/2026-10-18
  ?parameters=accounts:last_login_time,accounts:is_suspended

このレポートには、次の癖があります。

  • 日付は太平洋標準時(UTC-8)基準で、yyyy-mm-dd で指定する
  • 取れる期間は、既定でも最大でも直近450日
  • filters で accounts:last_login_time に比較演算子を付けて絞り込める。Directory APIの query とは違い、ログイン時刻での絞り込みができる

SSOでGoogleにログインしている組織では、timestamp_last_sso も並べて見ておくと、判定の見落としを減らせます。Directory APIの lastLoginTime がSSO経由のログインをどう扱うかは、Userリソースのページに記載がありません。両方を取って、食い違うユーザーだけを人の目で確かめる進め方が堅実です。

ライセンスの割り当てと突き合わせる

削減したいのは、アカウントの数ではなく契約しているライセンスの数です。割り当て状況は、Enterprise License Manager APIの licenseAssignments.listForProduct で取れます。

GET https://licensing.googleapis.com/apps/licensing/v1/product/Google-Apps/users
  ?customerId=C00000000&maxResults=100

Google Workspaceのプロダクト識別子は Google-Apps で、SKUはプランごとに分かれています。たとえばBusiness Starterの識別子は 1010020027 です。必要なスコープは https://www.googleapis.com/auth/apps.licensing で、Directory APIとは別になります。顧客が停止中だとエラーが返る点にも注意が必要です。

この結果をメールアドレスをキーにして users.csv と結合し、SKUの列を足すと、「高額なプランを持つ長期未ログインのユーザー」から順に見られます。Claude Codeには、CLAUDE.md に「SKUの列で並べ替え、プラン別の件数を併記」と1行足すだけで頼めます。

停止してもライセンス料は止まらない

候補が見つかっても、「停止すれば費用が減る」とは限りません。管理コンソールのヘルプには、停止したアカウントも、年間契約と柔軟プランのどちらでも、有効なアカウントと同じ料金がかかると書かれています。不要なアカウントの料金を止めたい場合は、停止ではなく削除する選択肢があります。

操作データ料金戻せるか
停止データメール・ドキュメント・カレンダーは削除されない料金有効なアカウントと同額戻せるかいつでも復元できる
削除データヘルプの削除手順で扱いが決まる料金止められる戻せるか直近20日以内に削除したユーザーは、showDeleted=true で取得して復元の手順に進める

停止中のアカウントでは、本人のGoogle Workspaceサービスが使えなくなります。共有されたドキュメントは、共同編集者が引き続き使えます。Google Playの開発者アカウントやBrand accountのように、停止で本人が使えなくなる資産もあるため、削除に進む前の確認が欠かせません。

この性質から、確認リストは2段にすると運用しやすくなります。

  1. 長期未ログインの候補全員に、本人または上長への確認を回す
  2. 返答があり不要と決まった人だけを、停止して数週間置き、削除に進める

停止・削除の前に確かめる項目

候補の一覧には、次の観点を列として足しておきます。orgUnitPath ごとに確認先を分けると、部門の管理者へそのまま回せます。

  • そのアカウントは、まだ在籍している人のものか
  • 共有メールボックスや、代表アドレスの受け口として使われていないか
  • 他のサービスのログインや通知先として使われていないか
  • ドキュメントなどの所有者になっていて、先に引き継ぎが要らないか
  • 育児休業や長期の休暇で、一時的に使っていないだけではないか

人がログインしない用途のアカウントは、lastLoginTime が古くても使われていることがあります。共有用のアカウントは、orgUnitPath を専用の部門に分けておくと、最初から判定の対象外にできます。

CSVにはメールアドレスが並びます。社内規程でAPIへの送信が制限されている場合は、アドレスを連番に置き換えた版を作り、対応表だけを手元に残して渡します。

月次に乗せるときの組み方

毎月の棚卸しにするなら、users.csv を日付つきのフォルダーに保存し、CLAUDE.md の基準日だけを書き換えます。翌月に同じアカウントが残っているかを見れば、「休暇による一時的な不在」と「使われなくなったアカウント」を分けて追えます。

同じ棚卸しの発想はMicrosoft 365にも当てはまります。Entra IDのサインイン情報から未使用ライセンスを探す手順はMicrosoft 365の未使用ライセンスをClaude Codeで洗い出す手順で、オンプレミスのディレクトリの休眠アカウントならActive Directory休眠アカウントの棚卸しで扱っています。契約や請求の側から棚卸しをするなら、SaaSの棚卸しの手順が近い構成です。

この記事を共有:XはてブLinkedIn