GitHub AgentScan

Show AgentScan info for GitHub users

Per 24-08-2026. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         GitHub AgentScan
// @version      0.1.1
// @description  Show AgentScan info for GitHub users
// @license      MIT
// @author       Bjorn Lu
// @homepageURL  https://github.com/bluwy/github-agentscan-userscript
// @supportURL   https://github.com/bluwy/github-agentscan-userscript/issues
// @namespace    https://github.com/bluwy
// @match        https://github.com/**
// @icon         https://www.google.com/s2/favicons?sz=64&domain=github.com
// @grant        GM.xmlHttpRequest
// @connect      github-agentscan-userscript.bjornlu.workers.dev
// @inject-into  content
// ==/UserScript==

(() => {
  // node_modules/.pnpm/[email protected]/node_modules/flru/dist/flru.mjs
  function flru_default(max) {
    var num, curr, prev;
    var limit = max || 1;
    function keep(key, value) {
      if (++num > limit) {
        prev = curr;
        reset(1);
        ++num;
      }
      curr[key] = value;
    }
    function reset(isPartial) {
      num = 0;
      curr = /* @__PURE__ */ Object.create(null);
      isPartial || (prev = /* @__PURE__ */ Object.create(null));
    }
    reset();
    return {
      clear: reset,
      has: function(key) {
        return curr[key] !== void 0 || prev[key] !== void 0;
      },
      get: function(key) {
        var val = curr[key];
        if (val !== void 0) return val;
        if ((val = prev[key]) !== void 0) {
          keep(key, val);
          return val;
        }
      },
      set: function(key, value) {
        if (curr[key] !== void 0) {
          curr[key] = value;
        } else {
          keep(key, value);
        }
      }
    };
  }

  // src/utils-fetch.ts
  function fetchJson(input, init) {
    return new Promise((resolve, reject) => {
      GM.xmlHttpRequest({
        ...getSharedOptions(input, init, reject),
        responseType: "json",
        onload: (response) => {
          resolve(response.response);
        }
      });
    });
  }
  function getSharedOptions(input, init, reject) {
    return {
      ...init,
      url: input instanceof Request ? input.url : input.toString(),
      // @ts-expect-error
      method: init?.method || "GET",
      headers: init?.headers ? init.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : Array.isArray(init.headers) ? Object.fromEntries(init.headers) : init.headers : void 0,
      data: init?.body?.toString() ?? void 0,
      onerror: (err) => reject(err),
      ontimeout: () => reject(new Error("Request timed out")),
      onabort: () => reject(new Error("Request aborted"))
    };
  }

  // src/identify-result.ts
  var cache = flru_default(30);
  async function getIdentifyResult(username) {
    const cached = cache.get(username);
    if (cached) return cached;
    const promise = fetchJson(`${"https://github-agentscan-userscript.bjornlu.workers.dev"}/identify/${username}`);
    cache.set(username, promise);
    promise.then((result) => cache.set(username, result));
    return promise;
  }

  // src/utils-debounce.ts
  function debounce(cb, delay) {
    let t;
    return () => {
      if (t != null) clearTimeout(t);
      t = setTimeout(cb, delay);
    };
  }

  // src/utils-wait.ts
  var inViewResolvers = /* @__PURE__ */ new Map();
  var inViewIntersectionObserver = new IntersectionObserver((entries) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      const element = entry.target;
      inViewIntersectionObserver.unobserve(element);
      inViewResolvers.get(element)?.resolve();
      inViewResolvers.delete(element);
    }
  });
  var inViewRemovalObserver = new MutationObserver(
    debounce(() => {
      for (const [element, resolvers] of inViewResolvers) {
        if (element.isConnected) continue;
        inViewIntersectionObserver.unobserve(element);
        inViewResolvers.delete(element);
        resolvers.reject(new Error("Element was removed from the DOM"));
      }
    }, 200)
  );
  inViewRemovalObserver.observe(document, { childList: true, subtree: true });
  function waitUntilInView(element) {
    const p = promiseWithResolvers();
    if (!element.isConnected) {
      p.reject(new Error("Element is not connected to the DOM"));
      return p.promise;
    }
    inViewResolvers.set(element, p);
    inViewIntersectionObserver.observe(element);
    return p.promise;
  }
  function promiseWithResolvers() {
    let resolve = () => {
    };
    let reject = () => {
    };
    const promise = new Promise((res, rej) => {
      resolve = res;
      reject = rej;
    });
    return { promise, resolve, reject };
  }

  // src/features/identify.ts
  var analyzedUserElements = /* @__PURE__ */ new WeakSet();
  function identify() {
    const authorEls = document.querySelectorAll(
      [
        // PR
        "a.author",
        // Issue
        'a[class*="IssueBodyHeaderAuthor"]',
        'a[class*="ActivityHeader-module__AuthorName"]',
        // PR list
        ".opened-by > a",
        // Issue list
        'a[class*="IssueItem-module__authorCreatedLink"]',
        // Home page
        'a.Link[data-hovercard-type="user"][data-octo-dimensions="link_type:self"]',
        // Commits page
        'a[class*="AuthorAvatar-module__authorHoverableLink"]',
        'a[class*="AuthorLink-module__authorNameLink"]'
      ].join(", ")
    );
    for (const authorEl of authorEls) {
      if (analyzedUserElements.has(authorEl)) continue;
      analyzedUserElements.add(authorEl);
      const parent = authorEl.parentElement;
      if (!parent) continue;
      const isAlreadyBot = Array.from(
        parent.querySelectorAll(
          [
            // PR
            "span.Label",
            // Issue
            'span[data-component="Label"]',
            // PR list
            "span.tooltipped > span.Label"
          ].join(", ")
        )
      ).find((label) => {
        const text = label.textContent?.trim().toLowerCase();
        return text === "ai" || text === "bot";
      });
      if (isAlreadyBot) continue;
      const username = authorEl.textContent?.trim();
      if (!username) continue;
      if (username.endsWith("[bot]")) continue;
      identifyUsername(username, authorEl).catch((err) => {
        console.error("Error fetching identify result for user", username, err);
      });
    }
  }
  async function identifyUsername(username, authorEl) {
    try {
      await waitUntilInView(authorEl);
    } catch {
      return;
    }
    const identifyResult = await getIdentifyResult(username);
    const agentscanLink = `https://agentscan.tools/user/${username}`;
    let label = null;
    if (identifyResult.isCommunityFlagged) {
      label = createLabel(
        "AI",
        agentscanLink,
        "Label--danger",
        "This user has been flagged as AI by the community"
      );
    } else if (identifyResult.classification === "automation") {
      label = createLabel(
        "AI",
        agentscanLink,
        "Label--severe",
        "This user has been flagged as automation by AgentScan"
      );
    } else if (identifyResult.classification === "mixed") {
      label = createLabel(
        "AI",
        agentscanLink,
        "Label--warning",
        "This user has been flagged as mixed by AgentScan"
      );
    } else {
    }
    if (label) {
      authorEl.insertAdjacentElement("afterend", label);
    }
  }
  function createLabel(text, link, labelClass, description) {
    const label = document.createElement("span");
    label.className = "tooltipped tooltipped-n";
    label.ariaLabel = description;
    label.dataset.viewComponent = "true";
    const child = document.createElement("a");
    child.className = ["ml-1 Label", labelClass].filter(Boolean).join(" ");
    child.textContent = text;
    child.href = link;
    child.target = "_blank";
    label.appendChild(child);
    return label;
  }

  // src/features/report.ts
  var analyzedEls = /* @__PURE__ */ new WeakSet();
  function report() {
    handlePR();
  }
  function handlePR() {
    const reportButtons = document.querySelectorAll(
      [
        // Normal comment
        '.timeline-comment-header a[aria-label="Report abusive content"]',
        // Review comment
        '.timeline-comment-group a[aria-label="Report content"]'
      ].join(", ")
    );
    for (const button of reportButtons) {
      if (analyzedEls.has(button)) continue;
      analyzedEls.add(button);
      const header = button.closest(".timeline-comment-header, .timeline-comment-group");
      if (!header) continue;
      const username = header.querySelector("a.author")?.textContent?.trim();
      if (!username) continue;
      const userId = header.querySelector('img[src^="https://avatars.githubusercontent.com/u/"]')?.getAttribute("src")?.match(/\/u\/(\d+)\?/)?.[1];
      if (!userId) continue;
      buildReportIssueUrl(username, userId).then((url) => {
        const reportButton = button.cloneNode();
        reportButton.href = url;
        reportButton.target = "_blank";
        reportButton.textContent = "Report to AgentScan";
        reportButton.setAttribute("aria-label", "Report to AgentScan");
        delete reportButton.dataset.gaClick;
        delete reportButton.dataset.testSelector;
        button.insertAdjacentElement("afterend", reportButton);
      }).catch((err) => {
        console.error("Error building report issue URL for user", username, err);
      });
    }
  }
  async function buildReportIssueUrl(username, userId) {
    const result = await getIdentifyResult(username);
    const reason = `AgentScan classified this account as possible "${result.classification}" (score ${result.score}/100).`;
    const evidenceLines = [`- Flagged in: ${withoutBacklink(location.href)}`];
    const url = new URL("https://github.com/matteogabriele/agentscan/issues/new");
    url.searchParams.set("template", "report-automated-account.yml");
    url.searchParams.set("title", `[AUTOMATION] ${username}`);
    url.searchParams.set("username", username);
    url.searchParams.set("user-id", userId);
    url.searchParams.set("reason", reason);
    url.searchParams.set("evidence", evidenceLines.join("\n"));
    return url.toString();
  }
  function withoutBacklink(url) {
    try {
      const parsed = new URL(url);
      if (parsed.hostname === "github.com") {
        parsed.hostname = "redirect.github.com";
      }
      return parsed.toString();
    } catch {
      return url;
    }
  }

  // src/index.ts
  run();
  document.addEventListener("pjax:end", () => run());
  document.addEventListener("turbo:render", () => run());
  var observer = new MutationObserver(debounce(run, 200));
  observer.observe(document.body, { childList: true, subtree: true });
  function run() {
    identify();
    report();
  }
})();