case_599 人事・採用

勤怠データから36協定の上限超過を先読みするClaude Code実装

勤怠データから36協定の上限超過を先読みするClaude Code実装

月45時間・年360時間の上限規制に対し、勤怠CSVから残業累計・特別条項の残回数・複数月平均80時間をClaude Codeでチェックする実装を解説。厚労省一次情報準拠のスクリプト例と運用の落とし穴つき。

結論:36協定の上限チェックは、勤怠システムから吐いたCSVを「法定時間外労働」に正規化し、月45時間・年360時間などの上限ルールを決定的なコードで判定し、締め日を待たずに月中のペースから超過を先読みする仕組みまで含めて、Claude Codeで構築できます。AIに任せるのは正規化とレポート生成で、上限の数字そのものはコードに固定します。

  • 要点1:時間外労働の上限は原則月45時間・年360時間。特別条項があっても年720時間以内・月100時間未満(休日労働を含む)・2〜6か月平均80時間以内(休日労働を含む)・月45時間超は年6か月までを超えられない(厚生労働省)
  • 要点2:チェックの精度を決めるのは「所定外」と「法定外」の区別。勤怠CSVの残業列が何を意味しているかを確定させる正規化工程が実装の8割
  • 要点3:月次締め後の集計では遅い。15日時点のペースから月末着地を推定する「先読みアラート」までコード化して初めて、労務担当が動ける時間が生まれる

対象読者:人事・労務部門とAI活用を進める開発者・PM・経営企画

今日やること:自社の勤怠システムからCSVを1か月分エクスポートして、「残業時間」列が所定外労働と法定時間外労働のどちらを指しているか、勤怠システムの設定画面で確認する

先に断っておくと、本記事は複数の支援経験をもとに一般化した実装パターン解説です。登場する数値は想定モデルケースの試算で、特定企業の実測値ではありません。また、36協定や労働時間管理の法的な判断は最終的に社会保険労務士・労働基準監督署への確認が必要です。この記事が扱うのは「その手前の検知を機械化する」部分です。

労務管理のAI活用を支援していたとき、印象に残っている画面があります。労務担当者のExcelに貼られた「残業ウォッチリスト」です。毎月10日ごろ、前月の勤怠データが締まってから、45時間に近い社員を目視でマーキングして各部門長にメールする。つまり超過が起きた後にしか気づけない運用でした。本人も「先月の話を今ごろ言われても、と現場に言われるんですよね」とこぼしていました。

36協定の上限規制は2019年4月(中小企業は2020年4月)から罰則付きで施行されています。それでも運用が「締め後の集計」のままの会社は珍しくありません。勤怠SaaSのアラート機能はあるのに、複数拠点でシステムが分かれていたり、特別条項の発動回数の管理が紙のままだったりして、結局Excelに戻ってくる。この記事では、その隙間をClaude Codeで埋める実装を、コードと運用の両面から解説します。

先に法律の数字を固定する——36協定チェックで見る6つの上限

実装の前に、判定ルールの根拠になる数字を厚生労働省の一次情報で固定します。ここが曖昧なままコードを書くと、後で全部書き直しになります。

ルール 内容 根拠
原則上限 時間外労働は月45時間・年360時間以内 労働基準法36条(厚労省 働き方改革特設サイト)
特別条項の年間上限 臨時的な特別の事情があっても時間外労働は年720時間以内 同上
単月の絶対上限 時間外労働+休日労働で月100時間未満 同上
複数月平均 時間外労働+休日労働の2〜6か月平均がすべて80時間以内 同上
特別条項の発動回数 月45時間を超えられるのは年6か月まで 同上
割増賃金 時間外は25%以上、月60時間超の部分は50%以上(2023年4月から中小企業にも適用) 厚労省・都道府県労働局

ここで実装者が必ず押さえるべきポイントが2つあります。

1つ目は、上限ごとに「休日労働を含むかどうか」が違うことです。年720時間の上限は時間外労働のみで数えますが、月100時間未満と複数月平均80時間以内は「休日労働を含む」で数えます。つまり判定エンジンは「法定時間外」と「法定休日労働」を別の列として持っていないと正しく計算できません。

2つ目は、違反には罰則(6か月以下の懲役または30万円以下の罰金)が科されるおそれがあると厚労省が明記していることです。上限チェックは「あると便利な管理帳票」ではなく、罰則付きの法令要件に対する防波堤です。だからこそ、判定ロジックをAIの推論に任せず、決定的なコードに固定する必要があります。

なお、36協定そのものは様式第9号(特別条項付きは様式第9号の2)で労働基準監督署に届け出るもので、上限チェックの「協定値」はこの届出の写しから転記します。協定で月40時間と定めていれば、法律上の45時間ではなく40時間が自社の上限です。チェックすべきは法律の数字と協定の数字の厳しいほう——これも設計の前提になります。

勤怠システムのアラートだけでは、なぜ足りないのか

「それ、勤怠SaaSにアラート機能ありますよね?」とよく言われます。その通りで、単一システム・単一拠点で運用が完結しているなら、まずSaaS標準のアラートを使うべきです。Claude Codeで独自チェックを組む価値が出るのは、次のどれかに当てはまる場合です。

  1. 複数の勤怠システムが並存している — 買収・合併や拠点ごとの歴史的経緯で、本社はA社製・工場はB社製・出向者はExcelという構成。横串の集計はどのSaaSの画面にも出ない
  2. 複数月平均80時間の全ウィンドウ判定が弱い — 2か月・3か月・4か月・5か月・6か月平均の「すべて」が80時間以内という条件は、直近2か月だけ見る簡易アラートでは漏れる
  3. 特別条項の発動回数管理が別帳簿になっている — 「今年あと何回45時間を超えられるか」の残枠が、勤怠画面ではなく労務担当の頭の中にある
  4. 締め後にしか数字が確定しない — 月中の速報値からの着地予測(先読み)をやってくれない

もう1つ、入力データの質の話をしておきます。厚労省の「労働時間の適正な把握のために使用者が講ずべき措置に関するガイドライン」は、労働時間の把握はタイムカードやPCの使用時間の記録といった客観的な記録を基礎とすることを原則とし、記録は労働基準法第109条に基づき保存することを求めています。自己申告のExcelだけを集計しても、把握の原則を満たしているとは言えません。今回の実装はあくまで「客観的記録がCSVで出てくる」状態が前提です。

実装の全体像——正規化・判定・先読みの3層に分ける

アーキテクチャはシンプルで、経理業務の経費精算の規程照合で使ったのと同じ「正規化→決定的な判定→確認リスト生成」の骨格です。動作環境はClaude Code(2026年7月時点の最新版)、Python 3.12、pandas。判定層に機械学習は使いません。

attendance-check/
├── CLAUDE.md            # プロジェクトの前提と禁止事項
├── config/
│   ├── agreement.yaml   # 36協定の協定値(様式第9号の写しから転記)
│   └── mapping/         # 勤怠システム別の列マッピング定義
├── input/               # 勤怠CSV(社員IDに仮名化済みのもの)
├── scripts/
│   ├── normalize.py     # 列マッピング+法定外/休日労働の分離
│   ├── check_limits.py  # 6ルールの判定エンジン
│   └── forecast.py      # 月中ペースからの着地予測
├── tests/               # 境界値テスト(44.9h / 45.0h / 45.1h など)
└── output/              # 月次レポート(Markdown)

CLAUDE.mdには、判定の前提と「AIが勝手にやってはいけないこと」を明記します。ここに書いた制約は、セッションをまたいでも効き続けます。

# CLAUDE.md(抜粋)
## このプロジェクトの前提
- 上限値はすべて config/agreement.yaml から読む。コードに直書きしない
- 「残業時間」という語を使わない。必ず「法定時間外」「法定休日労働」
  「所定外(法定内)」のいずれかで呼ぶ
- 月100時間・複数月平均80時間の判定には法定休日労働を含める。
  年720時間・月45時間・年360時間の判定には含めない

## 禁止事項
- 上限値・割増率など法律由来の数字を推測で書かない。必ず出典を確認
- input/ のCSVに氏名・生年月日など個人情報が含まれていたら処理を止めて報告
- 判定結果から「違法である」等の法的評価を書かない(判定は事実の列挙まで)

実装コード例1:勤怠CSVの正規化——「残業」列の意味を確定させる

最初にして最大の難所は、勤怠CSVの「残業時間」列が何を意味しているかです。多くの勤怠システムでは、所定労働時間が7.5時間の会社の場合、17時から18時までの30分は「所定外だが法定内」の労働で、36協定の上限計算には入りません。ここを区別せずに残業列を合計すると、上限チェックが常に厳しめに誤報を出すチェッカーになります。逆に、日8時間・週40時間の法定基準で再計算せず所定基準のまま集計すると、今度は過少に出ます。

正規化スクリプトの骨格はこうなります。

import pandas as pd
import yaml

def normalize(csv_path: str, mapping_path: str) -> pd.DataFrame:
    """勤怠システム別のCSVを共通スキーマに変換する。
    共通スキーマ: employee_id, date, work_min,
                  statutory_ot_min(法定時間外), holiday_work_min(法定休日),
                  prescribed_ot_min(所定外・法定内)
    """
    mapping = yaml.safe_load(open(mapping_path, encoding="utf-8"))
    df = pd.read_csv(csv_path, encoding=mapping["encoding"])
    df = df.rename(columns=mapping["columns"])

    # 検証1: 法定外+法定内所定外+所定内 = 総労働時間 が成立するか
    calc = df["statutory_ot_min"] + df["prescribed_ot_min"] + df["within_min"]
    bad = df[(calc - df["work_min"]).abs() > 1]
    if len(bad) > 0:
        raise ValueError(f"労働時間の内訳が合わない行が{len(bad)}件。"
                         "列マッピングの定義を見直してください")

    # 検証2: 氏名らしき列が残っていたら停止(仮名化の徹底)
    ng_cols = [c for c in df.columns if c in ("氏名", "name", "社員名")]
    if ng_cols:
        raise ValueError(f"個人情報列が残っています: {ng_cols}")
    return df

ポイントは、変換しながら恒等式で自己検証することです。「内訳の合計=総労働時間」が崩れている行が出たら、それは列マッピングの解釈が間違っているサインなので、そこで止めます。この検証がないと、間違った前提のまま全社の判定が静かに狂います。

列マッピングの初期作成はClaude Codeに任せるのが速い部分です。実際のプロンプトはこう書いています。

input/sample_hq.csv の先頭50行を読んで、列名と値の分布から
各列が「総労働」「法定時間外」「法定休日」「所定外(法定内)」の
どれに対応するか仮説を立て、config/mapping/hq.yaml の案を作成して。
根拠(どの値の組み合わせからそう判断したか)を必ず添えること。
判断がつかない列は「不明」と明記し、勝手に割り当てないこと。
不足している情報があれば、最初に質問してから作業を開始してください。

AIの仮説はあくまで下書きで、最終確定は勤怠システムの設定画面と労務担当の確認です。ここを飛ばした失敗例は後述します。

実装コード例2:6ルールの判定エンジンと複数月平均の全ウィンドウ検査

正規化さえ済めば、判定エンジンは素直なコードです。肝は複数月平均の実装で、「2か月・3か月・4か月・5か月・6か月平均のすべてが80時間以内」を、対象月ごとに全ウィンドウで検査します。

LIMITS = load_yaml("config/agreement.yaml")
# 例: monthly_ot: 45, yearly_ot: 360, special_yearly_ot: 720,
#     abs_monthly_with_holiday: 100, avg_with_holiday: 80,
#     special_months_per_year: 6
# 協定値が法律より厳しい場合は協定値を採用する

def check_multi_month_avg(monthly: pd.DataFrame) -> list[dict]:
    """時間外+休日労働の2〜6か月平均がすべて80h以内かを検査する。
    monthly: employee_id, month, ot_h(法定時間外), holiday_h(法定休日) の月次集計
    """
    findings = []
    m = monthly.assign(total_h=monthly["ot_h"] + monthly["holiday_h"])
    for emp, g in m.groupby("employee_id"):
        g = g.sort_values("month").set_index("month")
        for window in range(2, 7):           # 2〜6か月の全ウィンドウ
            avg = g["total_h"].rolling(window).mean()
            for month, v in avg.dropna().items():
                if v > LIMITS["avg_with_holiday"]:
                    findings.append({
                        "employee_id": emp, "rule": f"複数月平均({window}か月)",
                        "month": str(month), "value_h": round(v, 1),
                        "limit_h": LIMITS["avg_with_holiday"],
                    })
    return findings

先読み(forecast.py)はもっと単純で、月の途中までの法定時間外の累計を営業日ベースで月末まで外挿するだけです。精度よりも「15日時点で着地45時間超ペースの人を部門長に知らせる」タイミングの価値が本体なので、線形外挿で十分機能します。予測はあくまで参考値として、レポート上も「ペース試算」と明記します。

特別条項の残枠管理も、判定エンジンの出力から機械的に導けます。年度内に月45時間(協定値がそれ未満ならその値)を超えた月を数え、6か月(協定の回数がそれ未満ならその回数)から引くだけです。「あと何回超えられるか」が毎月のレポートに自動で載るようになると、労務担当の管理帳簿が1つ消えます。

Claude Codeに投げているプロンプト集

実装と運用で実際に使っているプロンプトを5つ紹介します。どれも共通して、最後に事故防止の一行を付けています。

1. 境界値テストの生成

scripts/check_limits.py の6つの判定ルールに対して、境界値テストを
tests/test_limits.py に作成して。各ルールについて「上限ちょうど」
「上限+0.1h」「上限-0.1h」の3ケースを最低限含めること。
月100時間未満は「100.0はNG、99.9はOK」であることに注意。
期待値はテストコード内にコメントで根拠(どの上限のどちら側か)を書くこと。
仮定した点は必ず"仮定"と明記してください。

「未満」と「以内」の違いはテストで固定するのが一番確実です。月100時間は「未満」なので100.0ちょうどはアウト、月45時間は「45時間を超えることができるのは」という規定なので45.0ちょうどはセーフ側——この非対称をコードレビューだけで守り切るのは無理があります。

2. 協定書のルールマスタ化

docs/36kyotei_2026.pdf(様式第9号の2の写し)を読んで、
config/agreement.yaml の項目に転記して。
法律の上限より厳しい値が協定されている項目には strict: true を付け、
読み取れなかった項目は null にして「要確認」リストを別途出力すること。
数字と固有名詞は、根拠(写しのどの欄か)を添えてください。

3. 月次レポートの生成

output/findings_2026-07.json をもとに、労務担当向けの月次レポートを
Markdownで作成して。構成は「要対応(上限超過)」「注意(残枠1か月以下・
着地45h超ペース)」「特別条項の残枠一覧」の3節。
法的評価(違法・適法の断定)は書かず、事実と数値のみを列挙すること。
不足している情報があれば、最初に質問してから作業を開始してください。

4. 打刻異常の洗い出し

正規化済みの input/normalized_2026-07.parquet から、判定の信頼性を
落とすデータ品質問題を探して: 深夜0時をまたぐ打刻の日付割当、
休憩控除がゼロの9時間超勤務、同一日の重複行、退勤打刻の欠損。
検出ロジックはscripts/quality_check.pyとして残し、件数サマリを報告して。
仮定した点は必ず"仮定"と明記してください。

5. 複数システムのマッピング差分レビュー

config/mapping/ 配下の3ファイル(hq.yaml / factory.yaml / temp.yaml)を
比較して、同じ概念に違う定義をしている箇所(例: 法定休日の判定方法、
分単位の丸め)を表にまとめて。丸め方の違いは判定結果に効くので、
どちらに寄せるべきかの論点も添えること。
数字と固有名詞は、根拠(出典/計算式)を添えてください。

AIに任せてはいけないこと

この仕組みでClaude Codeがやるのは検知と整理までです。次の領域は最初から人間側に固定しています。

  • 上限値そのものの決定 — 法律と協定の数字はYAMLに人間が転記し、AIの生成コードがそれを参照する一方向にする。AIが「たしか45時間」と書いた数字を信じない
  • 法的評価と是正措置 — 「この状態は労基法違反か」「特別条項を発動すべきか」はレポートに書かせない。判定結果を持って社会保険労務士か労働基準監督署に確認するのが正しい動線です
  • 個人情報の取り扱い — 勤怠データは氏名を社員IDに置換してから扱い、健康状態・面談記録などのセンシティブ情報はこのパイプラインに入れない。所属組織の規程・コンプライアンスに従ってください
  • 労働時間の認定 — そもそも何が労働時間か(持ち帰り・移動・研修など)の認定は判定エンジンの外の論点で、専門家の領域です

想定される効果(試算)

以下は従業員300名・2システム並存・特別条項ありの企業を想定したモデルケースの試算です(実測値ではありません)。

指標 導入前(想定) 導入後(試算)
横串集計とウォッチリスト作成 月8時間(手作業) 月1時間(レポート確認のみ)
超過に気づくタイミング 翌月10日ごろ(締め後) 当月15日(ペース先読み)
複数月平均80時間の検査範囲 直近2か月のみ目視 2〜6か月の全ウィンドウ
特別条項の残枠管理 別Excel(更新漏れあり) 月次レポートに自動掲載

工数削減より本質的なのは2行目です。締め後に分かっても打てる手は謝罪と再発防止だけですが、15日に分かれば業務の割り振りを変えられます。チェックの価値は検知の精度ではなく検知のタイミングに出ます。

段階的導入のロードマップ

Phase 1(1〜2か月): 1システム分の正規化と6ルール判定を過去12か月分で回し、手集計の結果と突き合わせる。ここで列マッピングの誤りを全部潰す

Phase 2(2〜4か月): 全システムに展開し、月中の先読みレポートを部門長配信に組み込む。通知だけの試行期間を1か月置く

Phase 3(4〜6か月): 特別条項の残枠管理・打刻品質チェックを統合し、月次の労務会議の定型資料にする

同じ「締めの前倒し」という文脈では、経理の月次決算・残高照合の実装パターンが参考になります。勤怠の確定が早まると給与計算と月次決算のスケジュールも前に倒れるので、労務と経理のチェック基盤は実は地続きです。

【要注意】よくある失敗パターンと回避策

失敗1:「残業時間」列をそのまま合計する
❌ 勤怠CSVの残業列を月45時間と直接比較する
⭕ 所定外(法定内)と法定時間外を分離してから比較する
所定7.5時間の会社では、この混同だけで1人あたり月10時間前後も過大に数え、アラートが誤報だらけになって信頼を失います。逆パターン(所定基準のまま過少計上)はもっと危険です。

失敗2:休日労働の含む・含まないを1つのルールで書く
❌ 「時間外+休日」の合計値を全ルールに使い回す
⭕ 年720時間・月45時間・年360時間は時間外のみ、月100時間・複数月平均80時間は休日労働込みで、列を分けて判定する
上限規制で最も実装ミスが起きやすいのがここです。境界値テストで両系統を固定してください。

失敗3:締め後バッチだけで運用する
❌ 月初に前月分を判定して終わり
⭕ 月中の速報データで週次の先読みを回す
締め後の判定だけだと「記録するだけのチェッカー」になります。先読みがあって初めて労務担当が現場に介入できます。

失敗4:生の勤怠データを氏名付きのままAIに渡す
❌ 氏名・部署・健康情報入りのCSVをそのまま処理させる
⭕ 社員IDに仮名化した列だけの中間ファイルを作ってから渡し、正規化スクリプトに個人情報列の検知ガードを入れる
勤怠データは個人データです。仮名化を運用ルールではなくコードのガードで強制するのが今回の実装の作法です。

よくある質問

Q. 36協定の上限チェックと給与計算ソフトの残業アラートは何が違いますか?

見ている軸が違います。給与計算側のアラートは主に割増賃金の計算単位(当月の時間数)を見ますが、36協定の上限規制は年360時間・年720時間の年間累計、2〜6か月平均80時間、特別条項の発動回数という「月をまたぐ状態」を持っています。この状態管理こそ手作業で漏れやすく、コード化の価値が大きい部分です。

Q. 月45時間ちょうどは違反になりますか?

月45時間の原則上限は「超えることができるのは年6か月まで」という規定なので、45時間ちょうどは特別条項の発動回数に数えません。一方で月100時間の絶対上限は「100時間未満」なので100時間ちょうどでアウトです。この「以内」と「未満」の非対称は実装バグの温床なので、境界値テストで固定することを推奨します。個別の適法性判断は社会保険労務士や労働基準監督署に確認してください。

Q. 協定で法律より厳しい上限を定めている場合はどちらでチェックしますか?

厳しいほうです。36協定で月40時間と定めていれば、自社の上限は法律の45時間ではなく40時間です。実装上は、法律値をデフォルトにしつつ協定値(様式第9号・第9号の2の写しから転記)で上書きする2層構成にし、どちらが効いているかをレポートに明記すると監査対応も楽になります。

Q. 管理監督者や裁量労働制の社員も同じ判定に入れてよいですか?

同じ判定式には入れられません。適用される規制の枠組みが異なるためです。実装では雇用区分の列で判定ルールセットを分岐させ、対象外の区分は「上限判定の対象外だが労働時間の状況の把握対象」として別レポートにするのが実務的です。区分の設計自体は労務の専門判断が必要なので、社会保険労務士に確認したうえでルールマスタに落としてください。

Q. 導入にはどれくらいの期間がかかりますか?

1システム構成なら、正規化の確定と過去データ検証で1〜2か月、先読みレポートの運用定着まで3〜4か月が目安です(想定モデルの試算)。最大の変数は勤怠システムの数と列定義の複雑さで、実装よりも「残業列の意味の確定」に時間がかかります。逆に言えば、そこさえ確定すれば判定エンジン自体は数日で書けます。

まとめ:今日から始める3つのアクション

  1. 今日やること:勤怠システムの設定画面で「残業時間」列の定義(所定外か法定外か)を確認し、36協定届(様式第9号)の写しを手元に揃える
  2. 今週中:直近3か月の勤怠CSVをエクスポートし、氏名を社員IDに置換したうえで、時間外+休日労働の2〜3か月平均を一度計算してみる。80時間に近い人が何人いるかで、この仕組みの緊急度が分かります
  3. 今月中:Phase 1(1システム・過去12か月の再判定)を小さく回し、手集計との差分を労務担当と一緒にレビューする

勤怠×36協定チェックは、派遣元・派遣先管理台帳の仕組み化や経費精算の規程照合と同じ「法令要件を決定的なコードに固定し、AIは正規化と整理を担う」パターンの労務版です。1つ組めば、同じ骨格が労務の他の台帳にも横展開できます。


著者:佐藤傑(さとう・すぐる)
株式会社Uravation代表取締役。X(@SuguruKun_ai)フォロワー約10万人。100社以上の企業向けAI研修・導入支援。著書『AIエージェント仕事術』(SBクリエイティブ)。

参考・出典

ご質問・ご相談は お問い合わせフォーム からお気軽にどうぞ。

関連記事: 年5日の有給取得義務の未達をClaude Codeで先読みする実装

関連記事: 労働者名簿・賃金台帳・出勤簿の抜け漏れをClaude Codeで点検する

関連記事: 給与計算の検算をClaude Codeで仕組み化する実装パターン

Next Step

この事例を、自社の業務に置き換える。

対象業務、利用データ、評価基準、社内展開の順番まで整理すると、AI開発ツール導入の失敗を減らせます。

導入を相談する

チームで学ぶなら: Claude Code 法人研修(2日間ハンズオン) / 1人で習得するなら: 個別指導(週1マンツーマン)