# NousResearch/hermes-agent · Google Workspace OAuth disabled_client detection and live checks

## Open now

- Nothing open on GitHub.

These issues and PRs address how the Google Workspace integration detects and reports disabled or misconfigured OAuth clients, including hardening the --check flow, adding a live-check mode, and correcting misclassification of partial-scope 403 errors as disabled-client errors.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #21826. Verify it addresses your case.
- **Recommended:** read PR #21826 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-21826`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.67`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #19570: google-workspace: fresh Google account disabled shortly after official OAuth setup; setup.py --check still reports AUTHENTICATED while API call fails disabled_client

## Prior & competing PRs

- **#21826: fix(google-workspace): detect disabled_client in --check and add --check-live (salvage of #19643)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `scripts/release.py`, `skills/productivity/google-workspace/scripts/setup.py`
  - fix hunks:
  - `scripts/release.py`

    ```diff
    @@ -47,6 +47,7 @@
         "[redacted:email]": "qiyin-code",
         "[redacted:email]": "olisikh",
         "[redacted:email]": "leoneparise",
    +    "[redacted:email]": "ygd58",
         "[redacted:email]": "teknium1",
         "[redacted:email]": "thelumiereguy",
         "[redacted:email]": "counterposition",
    ```
  - `skills/productivity/google-workspace/scripts/setup.py`

    ```diff
    @@ -130,7 +130,33 @@ def _ensure_deps():
                 sys.exit(1)
     
     
    -def check_auth():
    +def check_auth_live():
    +    """Check auth with a real API call to detect disabled_client/account issues."""
    +    # quiet=True suppresses the "AUTHENTICATED" print from check_auth so the
    +    # final status line reflects the live-call outcome (OK or FAILED).
    +    if not check_auth(quiet=True):
    +        return False
    +    try:
    +        from googleapiclient.discovery import build
    +        from google.oauth2.credentials import Credentials
    +        creds = Credentials.from_authorized_user_file(str(TOKEN_PATH))
    +        service = build("calendar", "v3", credentials=[redacted:secret-assignment]
    +        service.calendarList().list(maxResults=1).execute()
    +        print("LIVE_CHECK_OK: Real API call succeeded.")
    +        return True
    +    except Exception as e:
    +        err_str = str(e).lower()
    +        if "disabled_client" in err_str or "invalid_client" in err_str:
    +            print(f"LIVE_CHECK_FAILED: OAuth client or account disabled: {e}")
    +            print("  1. Check Google Cloud Console for disabled OAuth client")
    +            print("  2. Check myaccount.google.com for account status")
    +            print("  3. Do NOT retry with a disabled account")
    +        else:
    +            print(f"LIVE_CHECK_FAILED: {e}")
    +        return False
    +
    +
    +def check_auth(quiet: bool = False):
         """Check if stored credentials are valid. Prints status, exits 0 or 1."""
         if not TOKEN_PATH.exists():
             print(f"NOT_AUTHENTICATED: No token at {TOKEN_PATH}")
    ```

    ```diff
    @@ -157,7 +183,8 @@ def check_auth():
                 print(f"AUTHENTICATED (partial): Token valid but missing {len(missing_scopes)} scopes:")
                 for s in missing_scopes:
                     print(f"  - {s}")
    -        print(f"AUTHENTICATED: Token valid at {TOKEN_PATH}")
    +        if not quiet:
    +            print(f"AUTHENTICATED: Token valid at {TOKEN_PATH}")
             return True
     
         if creds.expired and creds.refresh_token:
    ```

    ```diff
    @@ -174,10 +201,25 @@ def check_auth():
                     print(f"AUTHENTICATED (partial): Token refreshed but missing {len(missing_scopes)} scopes:")
                     for s in missing_scopes:
                         print(f"  - {s}")
    -            print(f"AUTHENTICATED: Token refreshed at {TOKEN_PATH}")
    +            if not quiet:
    +                print(f"AUTHENTICATED: Token refreshed at {TOKEN_PATH}")
                 return True
             except Exception as e:
    -            print(f"REFRESH_FAILED: {e}")
    +            err_str = str(e).lower()
    +            if "disabled_client" in err_str or "invalid_client" in err_str:
    +                print(f"OAUTH_CLIENT_DISABLED: {e}")
    +                print("  The OAuth client or Google account has been disabled.")
    +                print("  Steps to resolve:")
    +                print("    1. Check your Google Cloud Console — verify the OAuth client is not disabled")
    +                print("    2. Check if your Google account itself has been disabled at myaccount.google.com")
    +                print("    3. If the account is disabled, you can appeal at accounts.google.com/signin/recovery")
    +                print("    4. Do NOT retry API calls with a disabled account — this may worsen the situation")
    +                print("    5. If the OAuth client is disabled, create a new one in Google Cloud Console")
    +            elif "token_revoked" in err_str or "invalid_grant" in err_str:
    +                print(f"TOKEN_REVOKED: {e}")
    +                print("  Re-run setup to re-authenticate.")
    +            else:
    +                print(f"REFRESH_FAILED: {e}")
                 return False
     
         print("TOKEN_INVALID: [redacted:secret-assignment] setup.")
    ```

    ```diff
    @@ -384,6 +426,7 @@ def main():
         parser = argparse.ArgumentParser(description="Google Workspace OAuth setup for Hermes")
         group = parser.add_mutually_exclusive_group(required=True)
         group.add_argument("--check", action="store_true", help="Check if auth is valid (exit 0=yes, 1=no)")
    +    group.add_argument("--check-live", action="store_true", help="Check auth with a real API call (detects disabled_client)")
         group.add_argument("--client-secret", metavar="PATH", help="Store OAuth client_secret.json")
         group.add_argument("--auth-url", action="store_true", help="Print OAuth URL for user to visit")
         group.add_argument("--auth-code", metavar="CODE", help="Exchange auth code for token")
    ```

    ```diff
    @@ -393,6 +436,8 @@ def main():
     
         if args.check:
             sys.exit(0 if check_auth() else 1)
    +    if getattr(args, "check_live", False):
    +        sys.exit(0 if check_auth_live() else 1)
         elif args.client_secret:
             [redacted:secret-assignment]
         elif args.auth_url:
    ```
- **#19643: fix(google-workspace): detect disabled_client in --check and add --check-live**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#21826`

## Fix status

- Merged to main in PR #21826 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-08`.
