GitHub AgentScan

Show AgentScan info for GitHub users

As of 2026-08-27. See the latest version.

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         GitHub AgentScan
// @version      0.1.4
// @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 !== void 0) 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-username.ts
  function getUsername(authorEl) {
    const fromUrl = new URL(authorEl.href);
    const fromUrlPathnameParts = fromUrl.pathname.split("/");
    if (fromUrl.search === "" && fromUrlPathnameParts.length === 2) return fromUrlPathnameParts[1];
    const fromText = authorEl.textContent?.trim();
    if (fromText) return fromText;
  }

  // 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 alreadyLabeled = parent.querySelector("[data-github-agentscan-userscript]");
      if (alreadyLabeled) 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 = getUsername(authorEl);
      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);
    if (!identifyResult) return;
    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 (false) {
      label = createLabel("Human", agentscanLink, "Label--secondary", "Living and breathing");
    }
    if (label && authorEl.parentElement) {
      const parentStyle = getComputedStyle(authorEl.parentElement);
      const alreadyHasMargin = parentStyle.display === "flex" && parentStyle.gap.endsWith("px");
      if (!alreadyHasMargin) {
        ;
        label.childNodes[0].classList.add("ml-1");
      }
    }
    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";
    label.dataset.githubAgentscanUserscript = "";
    const child = document.createElement("a");
    child.className = ["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 authorEl = header.querySelector("a.author");
      if (!authorEl) continue;
      const username = getUsername(authorEl);
      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) => {
        if (!url) return;
        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);
    if (!result) return null;
    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
  var run = debounce(() => {
    identify();
    report();
  }, 200);
  document.addEventListener("pjax:end", () => run());
  document.addEventListener("turbo:render", () => run());
  var observer = new MutationObserver(() => run());
  observer.observe(document.body, { childList: true, subtree: true });
})();