はじめに
猫のいる宿を紹介するサイト「ねこ宿研究所」では、公開した記事をInstagramとThreadsでも紹介しています。
手作業で投稿を続けるのは難しいため、VPS上で動かしているHermes Agentの運用環境に、次の処理を行う自動投稿システムを作りました。
- 公開済み記事から投稿候補を選ぶ
- InstagramとThreadsで異なる投稿文を組み立てる
- Meta APIで投稿する
- 成功・失敗をDiscordへ通知する
- cronから決まった時刻に実行する
- 失敗時は二重投稿を避けながら最大3回まで再試行する
実際に運用を始めると、再試行機能を追加した際にシェルスクリプトの実行権限が消え、cronが全停止する事故も起きました。本記事では、最終的な構成だけでなく、なぜその設計にしたか、障害から何を学んだかもまとめます。
なお、この記事は2026年8月時点で実際に構築・運用した内容です。Meta APIの仕様や必要な権限は変更される可能性があります。
Hermes AgentにAPIトークンを直接扱わせない
Hermes Agentは、Discordから指示を受け、スキルやターミナルを使って作業できるAIエージェントです。
最初は「Hermes Agentに記事を選ばせ、そのままMeta APIを呼ばせればよい」と考えました。しかし、外部投稿を自由なシェル操作へ任せると、次のリスクがあります。
- 同じ投稿を二重送信する
- 誤ったアカウントへ投稿する
- タイムアウト時に、投稿済みか分からないまま再実行する
- Discordの曖昧な指示を投稿承認として扱う
- ログや会話へアクセストークンを出してしまう
そこで、Hermes AgentとMeta APIの間に、用途を限定したPython製の投稿ワーカーを置きました。
Discord
↓ 指示・完了通知
Hermes Agent
↓ 制限されたコマンドだけ実行
投稿候補選定スクリプト
↓ draft → approve → publish
投稿ワーカー
├─ Instagram API
└─ Threads API
AIエージェントは運用や改修の窓口ですが、定刻投稿そのものは決定的なPythonスクリプトとcronで動かします。これにより、毎回LLMを呼ばず、モデルの利用枠や回答の揺らぎにも依存しない構成になりました。
コンテナとデータの分離
Hermes AgentはWordPressとは別のDocker Composeプロジェクトとして動かしています。WordPressのコンテナやDockerソケットはマウントせず、Hermes用のデータディレクトリだけを渡します。
services:
hermes:
image: nousresearch/hermes-agent:<固定したバージョン>
restart: unless-stopped
command: ["gateway", "run"]
volumes:
- /home/example/.hermes:/opt/data
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
InstagramとThreadsのトークンは、リポジトリ外のファイルとして保存します。
~/.hermes/credentials/instagram-access-token
~/.hermes/credentials/threads-access-token
権限は600とし、投稿ワーカーはファイルを読むだけです。トークンをコマンド引数、キュー、監査ログへ入れないようにしました。
投稿ワーカーの状態管理
投稿ワーカーは、1回の投稿をJSONキューとして保存します。主な状態は次のとおりです。
awaiting_approval
↓
approved
↓
publishing
↓
published
失敗時: failed / needs_review
本文、記事URL、画像URL、記事更新日、記事MarkdownのSHA-256を承認対象に含めます。承認後に記事や本文が変わった場合、ハッシュが一致しないため投稿を止めます。
def approval_hash(record: dict) -> str:
payload = json.dumps(
approval_payload(record),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return hashlib.sha256(payload.encode()).hexdigest()
自動運転へ移行する前には、DiscordからキューIDを指定して承認する試験期間を設けました。InstagramとThreadsの両方で実投稿を確認してから、自動運転専用の承認主体を追加しています。
投稿候補は公開済みMarkdownから選ぶ
ねこ宿研究所では、WordPress記事の正本をMarkdownで管理しています。自動投稿では、フロントマターが次の条件を満たす記事だけを候補にします。
status: publish-
slugが記事URLと一致する -
title、description、fact_checkedがある - 同じプラットフォームで直近60日に使っていない
- 公開URLがHTTP 200を返す
Instagramでは、さらにサイトのWordPressメディアにあるJPEG画像を必要条件にしました。
image_pattern = re.compile(
r"!\[([^]]+)\]\((https://www\.example\.com/"
r"wp-content/uploads/[^)]+\.(?:jpg|jpeg))\)",
re.IGNORECASE,
)
外部サイトや予約サイトの画像は取得しません。記事内画像がWebPしかない場合は、その記事をInstagram候補から外すか、WordPress側にJPEG派生版を用意します。
また、Markdown上では公開扱いでも、WordPress側のURLがまだ存在しないケースがありました。候補決定前に記事URLと画像URLへHEADリクエストを送り、到達不能な候補はスキップします。
def first_reachable(platform: str, candidates: list[dict]) -> dict:
for item in candidates:
if not public_url_ok(item["url"]):
continue
if platform == "instagram" and not public_url_ok(
item["image_url"], "image/jpeg"
):
continue
return item
raise AutomationError("No unused article with reachable public URLs")
InstagramとThreadsの文章を分ける
同じ記事を紹介する場合でも、両プラットフォームへ同一文面を同時投稿しないようにしました。
Threadsは記事URLを本文へ含め、時間帯ごとに役割を分けています。
08:00 ねこ宿小ネタ
11:30 今日のねこ宿
15:00 猫・旅行ネタ
19:00 新着記事・宿紹介
22:00 軽い雑談・質問
Instagramはサイト内JPEG画像と代替テキストを付け、1日2回投稿します。
11:30 ねこ宿紹介
20:00 写真・記事・看板猫紹介
宿紹介では、猫に必ず会えるような表現を避けるため、次の趣旨を必ず入れます。
猫との時間は、猫の意思やその日の体調を尊重してお楽しみください。
禁止表現や文字数は投稿ワーカーでも検査します。プロンプトだけで守るのではなく、コード側でも止めるのがポイントです。
Meta APIでの投稿
Instagramは、画像コンテナを作成し、処理完了を確認してから公開します。
アカウント確認
↓
メディアコンテナ作成
↓
status_codeがFINISHEDになるまで確認
↓
media_publish
↓
投稿IDとpermalinkを取得
Threadsも、テキストコンテナを作成してから公開します。どちらも投稿前にAPIが返すユーザー名を検査し、想定したアカウントでなければ停止します。
最も注意したのはネットワークタイムアウトです。公開APIの呼び出し後に通信が切れると、「投稿に失敗した」のか「投稿は成功したが応答を受け取れなかった」のか判断できません。
この場合は自動再投稿せず、キューをneeds_reviewにして対象プラットフォームを一時停止します。
except TimeoutError:
record["status"] = "needs_review"
state["paused"][record["platform"]] = True
save_record(record)
Discordへ完了通知する
投稿成功後は、Discord Bot APIで非公開の運用チャンネルへ通知します。
✅ threadsへ自動投稿しました(th-travel)。
記事: 記事タイトル
キュー: TH-...
https://www.threads.com/...
Discordのメッセージには、キューIDをもとに作った数値nonceを付けています。通知処理を再実行した場合でも、同じ完了通知が重複しにくくするためです。
重要なのは、投稿成功後のDiscord通知失敗を「投稿失敗」と同一視しないことです。キューがすでにpublishedなら、通知の再試行はしても、SNS投稿は再実行しません。
cron失敗時の再試行をどう設計したか
運用開始後、「cronが失敗したら原因を確認して再投稿し、3回失敗したら諦める」機能を追加しました。
単純にコマンド全体を3回実行するのは危険です。1回目で投稿済みなのに応答だけ失われた場合、同じ内容を3回投稿する可能性があります。
そこで、各試行の前後でキューディレクトリを比較します。
新しいキューがない
→ 投稿処理へ入る前の失敗。最大3回まで再試行可能
新しいキューがpublished
→ SNS投稿は成功。再投稿しない
新しいキューがpublishing / failed / needs_review
→ Metaへ到達した可能性がある。停止して人が確認
プラットフォームがpaused
→ 再試行しない
ラッパーの骨格は次のようになりました。
for (( attempt=1; attempt<=3; attempt++ )); do
before=$(snapshot_queue)
if python3 social_automation.py "$platform" "$slot"; then
exit 0
fi
decision=$(retry_decision "$before")
case "$decision" in
published:*) exit 0 ;;
retry:*) sleep 30 ;;
*) exit 1 ;;
esac
done
「失敗したら再試行」ではなく、「外部へ副作用が発生していないと確認できる場合だけ再試行」と考える必要がありました。
実際に起きた障害:再試行機能を追加したらcronが全停止
再試行機能の追加はHermes AgentへDiscordから依頼しました。ロジック自体は安全側に作られていましたが、更新後のシェルスクリプトが600になり、実行権限が消えていました。
cronログには次のエラーが残っていました。
flock: failed to execute run_social_automation.sh: Permission denied
原因は単純ですが、Pythonの構文検査だけでは検出できません。修正後は、配備処理とスキルの運用手順へ次の確認を追加しました。
chmod 700 run_social_automation.sh
bash -n run_social_automation.sh
stat -c "%a %n" run_social_automation.sh
さらに、投稿を伴わない/bin/falseを自動処理の代わりに指定し、正確に3回で終了するテストも行いました。
status=1 attempts=3 gave_up=1
最後に実際のThreads投稿を1件実行し、キューがpublishedになったこと、Discord通知が届いたこと、連続失敗数が0へ戻ったことまで確認しました。
運用して分かったこと
1. AIエージェントと副作用のある処理を分ける
Hermes Agentは、調査、運用指示、スキル改善の窓口として便利です。一方、SNS投稿のように取り消しにくい処理は、引数と許可範囲を狭くしたワーカーへ閉じ込めた方が安全でした。
2. 再試行回数より「再試行してよい状態」の定義が重要
最大3回という数字だけでは二重投稿を防げません。外部APIへ到達する前か、到達した可能性があるかを、永続化した状態から判断する必要があります。
3. 投稿後通知は投稿処理と別の副作用
Discord通知が失敗しても、SNS投稿まで巻き戻ったわけではありません。工程ごとに成功状態を分けることで、安全に復旧できます。
4. AIによる改修でもUnixの基本確認は必要
コード差分、構文、テストに加えて、実行権限、cronサービス、実行ユーザー、ログの保存先まで確認する必要があります。今回の障害はchmodとstatで防げるものでした。
5. 完全自動化でも停止手段を残す
Instagram、Threads、全体を個別に停止できるフラグを用意しました。曖昧なAPI結果や連続失敗時は自動停止し、再開だけは明示的な運用指示を必要とします。
まとめ
Hermes Agentを使って、記事選定、Instagram・Threads投稿、Discord通知、cron運転までを一つの運用フローにまとめました。
実装で特に重要だったのは、AIに自由に投稿させることではなく、次の境界をコードで作ることでした。
- 投稿できるアカウント、URL、画像形式を限定する
- 投稿内容と記事ソースをハッシュで固定する
- 状態を永続化して二重投稿を防ぐ
- 結果が曖昧なら再試行せず停止する
- 外部副作用の前だけ最大3回再試行する
- 投稿結果をDiscordへ通知する
- スクリプトの実行権限まで配備後に検査する
AIエージェントによる自動化は、自由度を高くするほど便利になるとは限りません。外部へ書き込む部分を小さく、決定的に、止めやすくすることで、ようやく日常運用へ載せられると感じました。
本システムを運用している「ねこ宿研究所」は、看板猫のいる宿を調査・紹介するサイトです。本記事の構成は、そこで実際に公開している記事とSNS運用を対象に検証しました。