9 min read

Unsloth: Fine-Tune and Run LLMs Locally (GitHub, Scanned)

Fine-tune open models faster on less GPU memory, then run them locally in a desktop app or web UI.

Unsloth logo
✅
Scan: safe. Nothing malicious. Two things to know: the curl | sh installer is very large, can escalate to sudo for system packages and edits your shell profile, and unsloth start can run third-party agents' own install scripts after a warning. Scanned Sep 27, 2026; the full report is below.

Unsloth began as a library that makes fine-tuning open models cheaper: hand-written GPU kernels and memory tricks that, by the project's own figures, train about twice as fast with around 70 percent less VRAM, with no accuracy loss. LoRA, QLoRA, full fine-tuning, pretraining and reinforcement learning (GRPO, DPO) are all supported, and the free Colab notebooks are how a lot of people trained their first model. Its quantised GGUF uploads on Hugging Face are widely used on their own.

It has since grown into a full local AI workbench. Unsloth Studio, a web UI and desktop app, runs GGUF and MLX models, builds datasets from PDFs and spreadsheets, trains and exports models, serves an OpenAI-compatible API, and with unsloth start points Claude Code, Codex or OpenCode at a local model. The core library is Apache-2.0 and Studio is AGPL-3.0. It is built by Unsloth AI and moves fast: more than 9,000 commits and a steady stream of beta releases.

  • Repository: github.com/unslothai/unsloth
  • Licence: Apache-2.0 (Apache License 2.0)
  • Language: Python. Stars: 76.8K. Forks: 7,054. Last push: Sep 27, 2026.
  • Scan: safe, Sep 27, 2026, commit db60370

Who it is for

People who want to fine-tune an open model on their own data with a single consumer GPU or a free Colab, and anyone who wants one local app to download, chat with, train and serve models.

Getting started

1. Install Unsloth Studio on macOS, Linux or WSL (Windows: irm https://unsloth.ai/install.ps1 | iex, or take the desktop app from unsloth.ai/download)

curl -fsSL https://unsloth.ai/install.sh | sh

2. Launch the web UI

unsloth studio

3. Or install only the Python library, for fine-tuning in code inside a virtual environment

uv pip install unsloth --torch-backend=auto

The installer is a shell script of about 8,700 lines fetched from unsloth.ai. It installs uv if needed, can ask to run sudo apt-get for missing system packages (it asks first, and never prompts in a non-interactive run), and appends a PATH line to your shell profile. It is install.sh in the repository if you want to read it first.

Safety scan

We cloned unslothai/unsloth at commit db60370 on Sep 27, 2026 and ran the checks described on the GitHub Tools page: credential patterns, decode-and-execute code, install-time scripts, committed binaries, risky CI workflows, every host the code talks to, known vulnerabilities in pinned dependencies, and project hygiene. A person read every hit. This is what we found.

  • 14 secret hits, all fakes in tests: GitHub, GitLab and Hugging Face token shapes fed to Studio's log-redaction tests, and PEM headers in the desktop app's redaction code and the project's own package-scanner tests. Two Hugging Face tokens in memory-estimate-identity.test.ts look real because they were generated on purpose; a comment explains they are a constructed hash collision for a test.
  • 80 pattern hits in three groups. Most are tests of Studio's command screener, which lets a model run terminal commands and is checked against reverse shells, crontab persistence and base64 exec strings that the scanner reads as the real thing. The scripts/scan_packages.py hits are Unsloth's own supply-chain scanner describing what it looks for. The rest are the installer and in-app update hints printing curl | sh.
  • install.sh (8,655 lines) and studio/setup.sh (5,769) are the substance. They install uv from a pinned, digest-checked release where they can and fall back to astral.sh's script otherwise, may run sudo apt-get for missing build packages after a prompt, and append PATH lines to .zshrc, .bashrc or .profile. scripts/uninstall.sh reverses the install; docker/install_nvidia_toolkit.sh uses sudo to install NVIDIA's container toolkit, as its name says.
  • unsloth start installs a missing coding agent (Claude Code, OpenClaw, Hermes and others) by running that vendor's own installer, and first prints a security warning naming the source and asks. Convenient, and worth knowing it happens.
  • 49 known advisories, none critical, all in the Studio front end's npm lockfile (mostly build-time packages such as browserslist and brace-expansion) and the desktop app's Cargo.lock (quick-xml among them). There is no Python lockfile, so the library's Python dependencies were not checked. 59 workflows; two use pull_request_target without checking out the pull request, and all 73 third-party actions are pinned to a commit, which is rare. Dependabot and CodeQL configured; no security policy.

What the scanner counted

CheckResult
Secrets14 candidates found and read; see the notes above.
Suspicious code80 pattern hits found and read; every one is listed under the raw findings.
Install-time code1 Cargo build script. 14 installer scripts (one fetches and runs a remote script; one edits your shell profile; one can call sudo)
Committed binariesNone.
CI workflows59 workflows. 2 use pull_request_target, none check out the pull request head. 0 of 73 third-party actions pinned to a tag rather than a commit.
Network hosts40 distinct hosts referenced from source; most often github.com, download.pytorch.org, huggingface.co, evil.example. 58 URLs to a bare IP address, listed under the raw findings.
Known vulnerabilities49 advisories across 1,719 pinned packages: 0 critical, 18 high, 19 moderate, 4 low, 8 unrated. studio/backend/core/data_recipe/oxc-validator/package-lock.json: 51 packages, 0 advisories; studio/frontend/package-lock.json: 1,111 packages, 31 advisories; studio/package-lock.json: 12 packages, 0 advisories; studio/src-tauri/Cargo.lock: 673 packages, 18 advisories.
Project hygieneHas automated dependency updates, CodeQL, licence file, contributing guide. Missing security policy.
OpenSSF Scorecard4.9 out of 10, as of Sep 1, 2026.

The raw findings

Every hit the scanner wrote out, with a link to the exact line at the scanned commit. Secrets candidates are redacted.

Secret candidates (14, redacted)
WhereRuleMatch
studio/backend/tests/test_debug_log_redaction.py:67gitlab-tokenglpat-…RST (26 chars)
studio/backend/tests/test_inference_worker_stderr_tail.py:1104github-tokenghp_ab…789 (40 chars)
studio/backend/tests/test_inference_worker_stderr_tail.py:1112github-tokenghp_ab…789 (40 chars)
studio/backend/tests/test_llama_cpp_start_failure_classification.py:1006github-tokengithub…hij (40 chars)
studio/backend/tests/test_llama_cpp_start_failure_classification.py:1007github-tokengithub…hij (40 chars)
studio/backend/tests/test_llama_cpp_start_failure_classification.py:1009github-tokengithub…hij (40 chars)
studio/frontend/tests/hf-token-inventory-invalidation.test.ts:30huggingface-tokenhf_123…234 (37 chars)
studio/frontend/tests/hf-token-inventory-invalidation.test.ts:31huggingface-tokenhf_123…234 (37 chars)
studio/frontend/tests/memory-estimate-identity.test.ts:113huggingface-tokenhf_7Mq…oAi (37 chars)
studio/frontend/tests/memory-estimate-identity.test.ts:114huggingface-tokenhf_He6…AK8 (37 chars)
studio/src-tauri/src/diagnostics/redaction.rs:202private-key-----B…--- (27 chars)
tests/security/test_scan_packages.py:1237private-key-----B…--- (31 chars)
tests/security/test_scan_packages.py:1323private-key-----B…--- (27 chars)
tests/security/test_scan_packages.py:1324private-key-----B…--- (27 chars)
Pattern hits (80)
WhereRuleMatch
.github/scripts/agent-guides-install.sh:63download-piped-to-shell# curl|bash installers, retried at the curl layer. We download to a temp file
.github/scripts/agent-guides-install.sh:84download-piped-to-shellecho "[install] curl|bash $url attempt $i failed; backing off $((i * 10))s" | tee -a "$LOG"
.github/scripts/agent-guides-install.sh:94download-piped-to-shell# start.py install_hint: curl -fsSL https://claude.ai/install.sh | bash
.github/scripts/agent-guides-install.sh:118download-piped-to-shell# start.py install_hint: curl -fsSL https://openclaw.ai/install.sh | bash
.github/scripts/agent-guides-install.sh:128download-piped-to-shell# curl -fsSL .../NousResearch/hermes-agent/main/scripts/install.sh | bash
docker/Dockerfile:424download-piped-to-shell# zstd Ollama installer (`curl ollama.com/install.sh | sh`) extracts a zstd tarball
docker/install_nvidia_toolkit.sh:105download-piped-to-shell# Rootless Docker keeps its own daemon, which the steps below would miss; root sees a different one, and `curl | sudo bash` arrives root with DOCKER_HOST stripp…
docker/install_nvidia_toolkit.sh:121download-piped-to-shell# piped through `curl | bash` there is no file to re-run: $0 is just "bash"
install.sh:2575download-piped-to-shell# Our own pipe, not the installer's: `curl | sh` leaves this script's stdin pointing
install.sh:3113download-piped-to-shell_rr_cmd="curl -fsSL https://unsloth.ai/install.sh | sh -s --$_rr_args"
install.sh:3115download-piped-to-shell_rr_cmd="curl -fsSL https://unsloth.ai/install.sh | sh"
install.sh:3128download-piped-to-shellsubstep " wsl -d $_rr_target -- bash -lc 'curl -fsSL https://unsloth.ai/install.sh | sh'"
install.sh:7113download-piped-to-shellsubstep " curl -fsSL https://unsloth.ai/install.sh | sh -s -- --no-torch"
install.sh:8616download-piped-to-shellecho " curl -fsSL https://unsloth.ai/install.sh | sh"
scripts/install_rocm_wsl_strixhalo.sh:111download-piped-to-shell# consumes a piped `curl | sh` stdin.
scripts/install_rocm_wsl_strixhalo.sh:116powershell-encodedpowershell.exe -NoProfile -Command "winget install --id ${_sdk_id} -e --source winget --accept-source-agreements --accept-package-agreements --disable-interacti…
scripts/install_rocm_wsl_strixhalo.sh:341download-piped-to-shellnote " curl -fsSL https://unsloth.ai/install.sh | sh"
scripts/scan_npm_packages.py:1437decode-then-eval# Code-only scanning for JS/TS sources: blank comments before matching so an IOC host, `eval(atob)` example or campaign marker quoted in a comment cannot manufa…
scripts/scan_packages.py:1374download-piped-to-shell"Shell pipes remote code into an interpreter (curl|sh dropper)",
scripts/scan_packages.py:1460download-piped-to-shell"Workflow pipes remote code into a shell (curl|sh dropper)",
scripts/uninstall.sh:6download-piped-to-shell# Usage: curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.sh | sh
scripts/uninstall.sh:15download-piped-to-shellcurl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.sh | sh
scripts/uninstall.sh:20download-piped-to-shellcurl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.sh | sh -s -- --help
scripts/uninstall.sh:1161download-piped-to-shell# user re-exports the variable. Hint when neither is set, so `curl | sh` does not silently miss.
and 56 more
URLs to bare IP addresses (58)
WhereRuleMatch
studio/backend/tests/multi_account/factories.py:95ip-literal-url"http://8.8.8.8:9/mcp",
studio/backend/tests/multi_account/factories_providers.py:21ip-literal-urlMCP_URL = "http://8.8.8.8:9/mcp"
studio/backend/tests/test_account_provider_isolation.py:110ip-literal-url"url": "https://8.8.8.8/mcp",
studio/backend/tests/test_account_recipe_mcp.py:130ip-literal-urlbuilt = run_as(ALICE, build_model_providers, _provider_recipe("https://8.8.8.8/v1"))
studio/backend/tests/test_account_recipe_mcp.py:131ip-literal-urlassert [provider.endpoint for provider in built] == ["https://8.8.8.8/v1"]
studio/backend/tests/test_account_recipe_mcp.py:139ip-literal-urlrun_as(ALICE, build_model_providers, _provider_recipe("http://8.8.8.8/v1"))
studio/backend/tests/test_account_tool_isolation.py:539ip-literal-urlassert seen == [("https://8.8.8.8/mcp", "public.example", "public.example")]
studio/backend/tests/test_keyless_api_access_adversarial.py:580ip-literal-url("http://8.8.8.8:8888", "8.8.8.8:8888", False),
studio/backend/tests/test_lan_access_settings.py:365ip-literal-url("10.1.1.144", False, ["http://203.0.113.9:8888"]),
studio/backend/tests/test_lan_access_settings.py:378ip-literal-urlstate = SimpleNamespace(server_url = "http://203.0.113.9:8888")
studio/backend/tests/test_lan_access_settings.py:411ip-literal-urlstate = SimpleNamespace(server_url = "http://203.0.113.9:8888")
studio/backend/tests/test_lan_access_settings.py:464ip-literal-urlserver_url = "http://203.0.113.9:8888",
studio/backend/tests/test_lan_access_settings.py:495ip-literal-urlassert status["urls"] == ["http://192.168.1.24:8888", "http://64.227.100.5:8888"]
studio/backend/tests/test_lan_access_settings.py:496ip-literal-urlassert status["public_urls"] == ["http://64.227.100.5:8888"]
studio/backend/tests/test_lan_share_host_resolution.py:238ip-literal-urlassert "http://203.0.113.9:8888" in capsys.readouterr().out
studio/backend/tests/test_provider_base_url_validation.py:410ip-literal-url"http://100.100.100.200/latest/meta-data/",
studio/backend/tests/test_provider_base_url_validation.py:474ip-literal-urlassert validate_provider_base_url("https://1.1.1.1/v1") == "https://1.1.1.1/v1"
studio/backend/tests/test_provider_base_url_validation.py:474ip-literal-urlassert validate_provider_base_url("https://1.1.1.1/v1") == "https://1.1.1.1/v1"
studio/backend/tests/test_remote_image_url_fetch_behaviour.py:536ip-literal-urlassert sent == ["https://93.184.216.34/caf%C3%A9%201.png?q=%C3%A9"]
studio/backend/tests/test_sandbox_tools.py:1123ip-literal-url+ "a24('http://203.0.113.5/')"
studio/backend/tests/test_sandbox_tools.py:1141ip-literal-url"import requests\nf = requests.get\nf = print\nf('http://203.0.113.5/')",
studio/backend/tests/test_sandbox_tools.py:1151ip-literal-url"obj = make()\ngetattr(obj, 'get')('http://203.0.113.5/')",
studio/backend/tests/test_sandbox_tools.py:1174ip-literal-url"import requests\nd = {}\nd.update({'https': 'http://203.0.113.5'})\nrequests.get('https://pypi.org/')",
studio/backend/tests/test_sandbox_tools.py:1176ip-literal-url"import requests\nd = {}\nd |= {'a': 'http://203.0.113.5'}\nrequests.get('https://pypi.org/')",
and 34 more
Installer scripts (14)
  • .github/scripts/run-studio-indicator-browser.sh, 100 lines
  • .github/scripts/run-studio-permission-browser.sh, 114 lines
  • .github/scripts/run-studio-ui-lane.sh, 177 lines
  • docker/install_nvidia_toolkit.sh, 283 lines, uses sudo; talks to docs.docker.com, docs.nvidia.com, get.docker.com, nvidia.github.io, raw.githubusercontent.com
  • docker/run.sh, 416 lines, uses sudo; talks to raw.githubusercontent.com
  • install.ps1, 11,411 lines; talks to aka.ms, docs.astral.sh, download-r2.pytorch.org, download.pytorch.org, files.pythonhosted.org, github.com, nodejs.org, pypi.nvidia.com, pypi.org, pypi.org.corp.example, raw.githubusercontent.com, registry.npmjs.org, registry.npmmirror.com, releases.astral.sh, repo.amd.com, rocm.docs.amd.com, tuna.mirrors.cernet.edu.cn, unsloth.ai, www.amd.com, www.python.org
  • install.sh, 8,655 lines, uses sudo, edits your shell profile, fetches and runs a remote script; talks to astral.sh, docs.astral.sh, docs.unsloth.ai, download-r2.pytorch.org, download.pytorch.org, files.pythonhosted.org, fishshell.com, github.com, nodejs.org, pypi.org, raw.githubusercontent.com, registry.npmjs.org, registry.npmmirror.com, releases.astral.sh, repo.amd.com, repo.radeon.com, rocm.docs.amd.com, rocm.nightlies.amd.com, tuna.mirrors.cernet.edu.cn, unsloth.ai, www.amd.com, www.apple.com
  • scripts/install_gemma4_mlx.sh, 179 lines; talks to astral.sh, github.com
  • scripts/install_qwen3_6_mlx.sh, 221 lines; talks to astral.sh, github.com
  • scripts/install_rocm_wsl_strixhalo.sh, 342 lines, uses sudo, edits your shell profile, fetches and runs a remote script; talks to github.com, pypi.org, repo.amd.com, repo.radeon.com, unsloth.ai
  • scripts/uninstall.ps1, 1,411 lines; talks to raw.githubusercontent.com
  • scripts/uninstall.sh, 1,176 lines, uses sudo, edits your shell profile, fetches and runs a remote script; talks to raw.githubusercontent.com
  • studio/setup.ps1, 10,260 lines; talks to aka.ms, cmake.org, developer.nvidia.com, download-r2.pytorch.org, download.pytorch.org, files.pythonhosted.org, git-scm.com, github.com, nodejs.org, pypi.org, python.org, registry.npmjs.org, registry.npmmirror.com, releases.astral.sh, repo.amd.com, rocm.docs.amd.com, tuna.mirrors.cernet.edu.cn, unsloth.ai, your-mirror.example
  • studio/setup.sh, 5,769 lines, uses sudo, edits your shell profile, fetches and runs a remote script; talks to astral.sh, developer.nvidia.com, download-r2.pytorch.org, download.pytorch.org, files.pythonhosted.org, fishshell.com, github.com, nodejs.org, pypi.org, registry.npmjs.org, registry.npmmirror.com, releases.astral.sh, tuna.mirrors.cernet.edu.cn, unsloth.ai, your-mirror.example
Worst known vulnerabilities (24 of 49)
AdvisorySeverityPackageSummary
GHSA-rgw5-rvv9-x895highbrace-expansion@5.0.8brace-expansion: DoS via unbounded intermediate arrays, bypassing the CVE-2026-14257 mitigation
GHSA-3jxr-9vmj-r5cphighbrace-expansion@1.1.14brace-expansion: DoS via exponential-time expansion of consecutive non-expanding {} groups
GHSA-mh99-v99m-4gvghighbrace-expansion@1.1.14brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash
GHSA-rgw5-rvv9-x895highbrace-expansion@1.1.14brace-expansion: DoS via unbounded intermediate arrays, bypassing the CVE-2026-14257 mitigation
GHSA-73wf-gq98-2v4ghighbrowserslist@4.28.2Browserslist: Uncaught crash / prototype write via untrusted browserslist-stats.json custom stats (normalizeStats)
GHSA-c83g-rgw3-j3cxhighbrowserslist@4.28.2Browserslist: Unbounded memory growth (no cache eviction) via distinct query results, leading to eventual OOM
GHSA-5jgf-p345-68v8highfast-uri@3.1.4fast-uri vulnerable to host confusion via skipped IDN canonicalization on scheme-relative references
GHSA-7p8r-x3mc-p8w7highfast-uri@3.1.4fast-uri vulnerable to host confusion via backslash authority introducer
GHSA-f65p-4m7j-42xchighfast-uri@3.1.4fast-uri vulnerable to server-side request forgery via malformed IPv6 normalization
GHSA-fph4-wmhf-6fwfhighfast-uri@3.1.4fast-uri vulnerable to server-side request forgery via repeated hostname percent-decoding
GHSA-jqff-g426-hqxphighfast-uri@3.1.4fast-uri vulnerable to host confusion via percent-encoded scheme normalization
GHSA-28wg-ghj8-5hjvhighnanoid@5.1.11nanoid: non-secure generators can loop indefinitely with negative size
RUSTSEC-2026-0194highquick-xml@0.37.5Quadratic run time when checking a start tag for duplicate attribute names
RUSTSEC-2026-0195highquick-xml@0.37.5Unbounded namespace-declaration allocation in `NsReader` enables memory-exhaustion denial of service
RUSTSEC-2026-0194highquick-xml@0.38.4Quadratic run time when checking a start tag for duplicate attribute names
RUSTSEC-2026-0195highquick-xml@0.38.4Unbounded namespace-declaration allocation in `NsReader` enables memory-exhaustion denial of service
RUSTSEC-2026-0194highquick-xml@0.39.2Quadratic run time when checking a start tag for duplicate attribute names
RUSTSEC-2026-0195highquick-xml@0.39.2Unbounded namespace-declaration allocation in `NsReader` enables memory-exhaustion denial of service
GHSA-w5vr-8v7q-w6rvmoderatebaseline-browser-mapping@2.10.27baseline-browser-mapping process termination on invalid input causes denial of service
GHSA-54fx-42gc-7vw4moderatehono@4.12.25Hono: Algorithmic Complexity DoS in Language Middleware
GHSA-8j4g-w8fx-2239moderatehono@4.12.25Hono: ReDoS in CORS middleware via Access-Control-Request-Headers
GHSA-crvj-82cr-hjcxmoderatehono@4.12.25Hono: Query parser reads parameters after the URL fragment, causing cache-key and proxy interpretation differentials
GHSA-f23p-vx2j-j53rmoderatehono@4.12.25Hono: `memo()` retains SSR output across requests, leading to cross-user data disclosure
GHSA-g6gw-c38x-mqfcmoderatehono@4.12.25Hono: Unbounded dot-notation nesting in `parseBody()` can cause memory exhaustion
Workflows worth a look

By the numbers

Stars76.8K
Forks7,054
Contributors349
Commits9,372
Open issues802
Open pull requests472
Releases70
Latest releasev0.1.815-beta
LicenceApache-2.0
Main languagePython
Project age2 years
Last pushSep 27, 2026
Tracked files6,271
Lines of code2.7M
Checkout size133 MB

Lines by language: Python 1.9M, TypeScript 614.6K, Shell 47.3K, YAML 46K, Rust 39.3K, PowerShell 35.6K.

Questions

Is Unsloth free?

Yes. The core library is Apache-2.0 and Unsloth Studio, the web UI and desktop app, is AGPL-3.0, so both are free to use, including for training models you sell. The AGPL matters only if you modify Studio itself and offer it to others as a service. The Colab notebooks are free too, within Google's free-tier GPU limits.

What hardware do I need to fine-tune with Unsloth?

An NVIDIA GPU is the best-supported path; AMD and Intel GPUs have their own install guides, and Macs can run and train through MLX. How much VRAM you need depends on model size and method, and 4-bit QLoRA is what makes small and mid-size models practical on a single consumer card. Without a GPU, start with the free Colab notebooks.

What is the difference between Unsloth and Ollama?

Ollama runs models; Unsloth trains them and, through Studio, runs them as well. A common path is to fine-tune in Unsloth, export to GGUF, and load the result in Ollama or llama.cpp. If you only want to chat with existing models, Ollama is simpler. If you want datasets, training and export in one app, use Unsloth.


This post is part of GitHub Tools, where every repository is cloned and scanned before it is written up. The scan is a snapshot of one commit on one day; the repository has moved on since, so check it before you install.