Install
openclaw skills install @joeyang001/gmail-cleanupApproval-gated Gmail cleanup — triage your inbox into spam/info/action lanes, draft replies, and archive or trash only after you approve on Telegram.
openclaw skills install @joeyang001/gmail-cleanupApproval-gated Gmail triage: classify inbox mail into three lanes, propose cleanup and reply drafts, route every decision through Telegram, and execute only what the owner approves. Nothing is sent or trashed automatically.
Built on the gog skill (Google Workspace CLI). Requires the gog skill and a
configured Telegram channel.
export GOG_KEYRING_PASSWORD='…'
example alongside the secret-manager form. No script changes.GOG_KEYRING_PASSWORD per run from a secret manager; do not persist or commit it.GOG_KEYRING_PASSWORD must be supplied by the caller
(no fallback to ~/.config/gogcli/keyring.pass), and the Workspace CLI is invoked
as gog from PATH only (no GOG_BIN override).cleanup-execute.py now enforces the approval, dry-run,
and send gates in code: dry-run is the default; --apply is refused without an
explicit approval{approved,approvedBy,approvedAt} artifact; send-draft
additionally requires --send and per-action "sendApproved": true and
safety.blockAllSends == false.triage-scan.py — retry + exponential backoff on
Gmail 403 rateLimitExceeded, optional per-request pacing, and tolerance for
malformed/partial API responses (skipped, not fatal).Authenticate gog for the target account — gog auth add you@gmail.com
(confirm with gog auth list).
Provide the keyring password to the run (required). gog needs
GOG_KEYRING_PASSWORD in the environment to unlock its keyring without an
interactive prompt. The scripts require it and never read credential files
themselves. Export it for the run — ideally sourced from a secret manager rather
than typed in plaintext:
export GOG_KEYRING_PASSWORD='<your gog keyring password>' # current session only
# or, sourced from a secret manager:
export GOG_KEYRING_PASSWORD="$(pass show gog-keyring)" # current session only
Do not commit the secret or persist it in shell rc; prefer a secret manager or CI secret store for scheduled runs.
Put gog on PATH. The scripts call gog directly and accept no override.
Configure the Telegram channel in OpenClaw (channels.telegram) with the
owner's numeric user id in an allowlist. Outbound uses openclaw message send.
Create the state dir and config:
mkdir -p state/gmail-cleanup
cp skills/gmail-cleanup/templates/config.example.json state/gmail-cleanup/config.json
# then edit account + notify.target
| Lane | Meaning | Standing action |
|---|---|---|
spam_suspect | missed spam — cold outreach, lookalike/sketchy domains, scam phrasing, no prior thread | trash, on approval only |
info_only | informational, no action needed — receipts, shipping, statements, newsletters, notifications, noreply@, List-Unsubscribe present | mark-read + archive |
action_required | needs the owner — real humans asking something, bills/invoices with deadlines, security/account alerts, invites | keep + draft a reply |
Every lane's action and whether it needs approval are config-driven (see
templates/config.example.json). Defaults match the owner's standing choices:
archive for info-only, trash-on-approval for spam, draft-on-approval for replies.
state/gmail-cleanup/config.json — copy from templates/config.example.json.
Everything the workflow does is data-driven here; no skill edits needed to retune.
account — Gmail address.scope.query — Gmail search syntax; the scope knob (e.g. in:inbox,
in:inbox is:unread). scope.extraQueries[] merges additional queries
(e.g. category:promotions). De-duplicated by id.policy.<lane>.action — trash | archive | mark-read | none.policy.<lane>.requiresApproval — true/false.policy.action_required.draftReply / sendRequiresApproval.limits.scanMax — max candidates fetched per run (keep small; per-message get is API-costly).limits.maxActionsPerRun — hard cap on mutations per run.notify.channel / notify.target — where approval requests go.cadence.everyHours — run interval (default 4).safety.dryRunFirst, safety.blockAllSends — safety gates.GMAIL_CLEANUP_PACE_MS — delay between API requests (default 250).GMAIL_CLEANUP_RETRIES — max retries per request on 403 rateLimitExceeded (default 4).limits.scanMax modest (≈12–20). Gmail's per-user Total Query Cost is a
per-minute budget; per-message get calls are the expensive part.scripts/cleanup-execute.py refuses to mutate anything without an explicit
approval artifact embedded in the action spec:
{
"approval": {
"approved": true,
"approvedBy": "telegram:<owner-id>",
"approvedAt": "2026-10-12T01:00:00Z"
},
"actions": [ { "type": "archive", "id": "<msgId>" } ]
}
--apply.--apply is refused unless approval.approved is boolean true with
non-empty approvedBy and approvedAt (else it exits and logs a REFUSED line).send-draft runs only when --apply and
--send and safety.blockAllSends == false and the action itself has
"sendApproved": true. Otherwise it is skipped and logged.state/gmail-cleanup/approved.json before executing.scripts/triage-scan.py pulls candidates for the
configured scope, fetches headers/body per message, and writes
state/gmail-cleanup/latest.json. It mutates nothing.action_required — never auto-clean something ambiguous.state/gmail-cleanup/pending.json with the
cleanup batch and any reply drafts, then send a compact digest to Telegram via
scripts/notify-telegram.sh.approve cleanup, trash ok,
approve drafts 1,2, skip 3, edit 2 <note>). Parse the reply into a
resolved action list and an approval{} block (approvedBy = the owner's
Telegram identity, approvedAt = the reply time) — write both to
state/gmail-cleanup/approved.json. This artifact is the approval.scripts/cleanup-execute.py <approved.json> runs dry-run first
(default); re-run with --apply to mutate (refused without the approval
artifact). Archive/read for info-only; trash for approved spam; gog gmail drafts reply for drafts. Sending a draft needs --apply --send plus
"sendApproved": true on the action and safety.blockAllSends == false.state/gmail-cleanup/audit.log (timestamp, mode, action, id, result).spam_suspect — no prior thread with sender; lookalike/typo domains; Reply-To
mismatched with From; SPF/DKIM/DMARC fail in Authentication-Results; scam
phrasing ("you've won", crypto, pharma, urgency + attachment); unsolicited sales.
info_only — List-Unsubscribe / List-Unsubscribe-Post present; Precedence: bulk; noreply@ / notifications@ / no-reply@ senders; categories
Promotions/Social/Updates/Forums; receipts/statements/shipping; first name + no
question; "this is a receipt/confirmation/notification".
action_required — from a human (real name, personal or known org domain) in a thread the owner is part of; contains a direct question, request, deadline, or "confirm"/"action required"/"due"; bills/invoices with a due date; security or account alerts that ask to verify/act; calendar invites. Default bucket.
Signals are inputs to judgment, not hard rules. Prefer precision over volume: a wrong trash is worse than a missed cleanup.
cleanup-execute.py, not
merely documented: no --apply without an approval{} artifact, no send without
--apply --send + sendApproved:true + blockAllSends=false.--apply is a separate, explicit step.action_required (surfaced, not cleaned).limits.maxActionsPerRun).safety.blockAllSends is a hard kill-switch for the send step.--gmail-no-send on gog except during an owner-approved send.Every cadence.everyHours (default 4). Preferred scheduler: an OpenClaw cron
isolated job with delivery.mode: announce to the Telegram channel, payload
agentTurn invoking this skill's workflow. If no channel is available, fall back
to a heartbeat tasks: entry in HEARTBEAT.md.
state/gmail-cleanup/
config.json # policy (from templates/config.example.json)
latest.json # scan output
pending.json # proposal awaiting approval
approved.json # resolved action list + approval{} artifact
audit.log # append-only action log
scripts/triage-scan.py — read-only scan → latest.json. Retries with
exponential backoff on Gmail 403 rateLimitExceeded, paces per-message get
calls (GMAIL_CLEANUP_PACE_MS), and skips malformed/partial responses instead of
crashing; reports a skipped count. Credentials from env only.scripts/cleanup-execute.py — dry-run/apply an approved action list → audit.log.
Enforces the approval artifact for --apply, the --send+sendApproved gate for
sends, and backs off on rate limits.scripts/notify-telegram.sh — send a digest to Telegram via openclaw message send.