Guides

In CI and AI agents

Where files are written by code, nobody opens them before a customer does. Put the check where the file is made.

Fail the build

Store the key as a repository secret named IHCF_API_KEY, then check every file the build produced. This job fails when any of them would not open cleanly.

GitHub Actions
# .github/workflows/office-files.yml
name: Office files open
on: [pull_request]

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build the reports
        run: python build_reports.py        # writes out/*.xlsx
      - name: Check every file
        env:
          IHCF_API_KEY: ${{ secrets.IHCF_API_KEY }}
        run: |
          status=0
          for f in out/*.xlsx out/*.docx out/*.pptx; do
            [ -e "$f" ] || continue
            verdict=$(curl -sf https://api.ihatecorruptfiles.xyz/v1/check \
              -H "Authorization: Bearer $IHCF_API_KEY" \
              -F "file=@$f" | jq -r .verdict)
            echo "$f: $verdict"
            case "$verdict" in ok|tolerated|unconfirmed) ;; *) status=1 ;; esac
          done
          exit $status

Each file is one request, so a build that writes 20 files uses 20. The free plan's 500 a month suits a pipeline that runs on pull requests, not on every commit to every branch.

Give an agent the check

An LLM writing Office files can't see Excel's repair prompt. Give it a tool that can: the findings and planned repairs are written to be read, so the agent can fix its own code and try again — or you can send the file to /v1/repair.

Python · agent tool
import os, requests

def check_office_file(path: str) -> dict:
    """Check that an .xlsx, .docx or .pptx opens cleanly in Microsoft Office.
    Returns the verdict and what is wrong, so the file can be fixed and checked again."""
    with open(path, "rb") as f:
        r = requests.post(
            "https://api.ihatecorruptfiles.xyz/v1/check",
            headers={"Authorization": f"Bearer {os.environ['IHCF_API_KEY']}"},
            files={"file": f},
            timeout=120,
        )
    r.raise_for_status()
    result = r.json()
    return {
        "verdict": result["verdict"],
        "opens_in_office": result["opens_in_office"],
        "problems": [f["message"] for f in result["findings"] if f["severity"] == "blocks"],
        "repairs": [a["description"] for a in result["actions"] if not a["advisory"]],
    }

The docstring is the tool's description for the model. Keep it: it tells the agent when to call the tool and what to do with the answer.

Retrying well

429 rate_limited, 503 at_capacity and the 5xx gateway errors are worth retrying; wait for Retry-After when it is sent. quota_exceeded and daily_limit are not — their Retry-After is hours or days away. Failed requests on our side are not counted, so retrying them costs nothing.

Python
import random, time, requests

def post_with_retry(url, attempts=4, **kwargs):
    for attempt in range(attempts):
        response = requests.post(url, **kwargs)
        if response.status_code not in (429, 502, 503, 504):
            return response
        code = response.json().get("code")
        if code in ("quota_exceeded", "daily_limit"):
            return response                     # waiting minutes won't help
        wait = float(response.headers.get("Retry-After", 2 ** attempt))
        time.sleep(wait + random.random())
    return response

Limits and quotas has the numbers.