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 | sh2. Launch the web UI
unsloth studio3. Or install only the Python library, for fine-tuning in code inside a virtual environment
uv pip install unsloth --torch-backend=autoThe 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
| Check | Result |
|---|---|
| Secrets | 14 candidates found and read; see the notes above. |
| Suspicious code | 80 pattern hits found and read; every one is listed under the raw findings. |
| Install-time code | 1 Cargo build script. 14 installer scripts (one fetches and runs a remote script; one edits your shell profile; one can call sudo) |
| Committed binaries | None. |
| CI workflows | 59 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 hosts | 40 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 vulnerabilities | 49 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 hygiene | Has automated dependency updates, CodeQL, licence file, contributing guide. Missing security policy. |
| OpenSSF Scorecard | 4.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)
Pattern hits (80)
| Where | Rule | Match |
|---|---|---|
| .github/scripts/agent-guides-install.sh:63 | download-piped-to-shell | # curl|bash installers, retried at the curl layer. We download to a temp file |
| .github/scripts/agent-guides-install.sh:84 | download-piped-to-shell | echo "[install] curl|bash $url attempt $i failed; backing off $((i * 10))s" | tee -a "$LOG" |
| .github/scripts/agent-guides-install.sh:94 | download-piped-to-shell | # start.py install_hint: curl -fsSL https://claude.ai/install.sh | bash |
| .github/scripts/agent-guides-install.sh:118 | download-piped-to-shell | # start.py install_hint: curl -fsSL https://openclaw.ai/install.sh | bash |
| .github/scripts/agent-guides-install.sh:128 | download-piped-to-shell | # curl -fsSL .../NousResearch/hermes-agent/main/scripts/install.sh | bash |
| docker/Dockerfile:424 | download-piped-to-shell | # zstd Ollama installer (`curl ollama.com/install.sh | sh`) extracts a zstd tarball |
| docker/install_nvidia_toolkit.sh:105 | download-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:121 | download-piped-to-shell | # piped through `curl | bash` there is no file to re-run: $0 is just "bash" |
| install.sh:2575 | download-piped-to-shell | # Our own pipe, not the installer's: `curl | sh` leaves this script's stdin pointing |
| install.sh:3113 | download-piped-to-shell | _rr_cmd="curl -fsSL https://unsloth.ai/install.sh | sh -s --$_rr_args" |
| install.sh:3115 | download-piped-to-shell | _rr_cmd="curl -fsSL https://unsloth.ai/install.sh | sh" |
| install.sh:3128 | download-piped-to-shell | substep " wsl -d $_rr_target -- bash -lc 'curl -fsSL https://unsloth.ai/install.sh | sh'" |
| install.sh:7113 | download-piped-to-shell | substep " curl -fsSL https://unsloth.ai/install.sh | sh -s -- --no-torch" |
| install.sh:8616 | download-piped-to-shell | echo " curl -fsSL https://unsloth.ai/install.sh | sh" |
| scripts/install_rocm_wsl_strixhalo.sh:111 | download-piped-to-shell | # consumes a piped `curl | sh` stdin. |
| scripts/install_rocm_wsl_strixhalo.sh:116 | powershell-encoded | powershell.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:341 | download-piped-to-shell | note " curl -fsSL https://unsloth.ai/install.sh | sh" |
| scripts/scan_npm_packages.py:1437 | decode-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:1374 | download-piped-to-shell | "Shell pipes remote code into an interpreter (curl|sh dropper)", |
| scripts/scan_packages.py:1460 | download-piped-to-shell | "Workflow pipes remote code into a shell (curl|sh dropper)", |
| scripts/uninstall.sh:6 | download-piped-to-shell | # Usage: curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.sh | sh |
| scripts/uninstall.sh:15 | download-piped-to-shell | curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.sh | sh |
| scripts/uninstall.sh:20 | download-piped-to-shell | curl -fsSL https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.sh | sh -s -- --help |
| scripts/uninstall.sh:1161 | download-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)
| Where | Rule | Match |
|---|---|---|
| studio/backend/tests/multi_account/factories.py:95 | ip-literal-url | "http://8.8.8.8:9/mcp", |
| studio/backend/tests/multi_account/factories_providers.py:21 | ip-literal-url | MCP_URL = "http://8.8.8.8:9/mcp" |
| studio/backend/tests/test_account_provider_isolation.py:110 | ip-literal-url | "url": "https://8.8.8.8/mcp", |
| studio/backend/tests/test_account_recipe_mcp.py:130 | ip-literal-url | built = run_as(ALICE, build_model_providers, _provider_recipe("https://8.8.8.8/v1")) |
| studio/backend/tests/test_account_recipe_mcp.py:131 | ip-literal-url | assert [provider.endpoint for provider in built] == ["https://8.8.8.8/v1"] |
| studio/backend/tests/test_account_recipe_mcp.py:139 | ip-literal-url | run_as(ALICE, build_model_providers, _provider_recipe("http://8.8.8.8/v1")) |
| studio/backend/tests/test_account_tool_isolation.py:539 | ip-literal-url | assert seen == [("https://8.8.8.8/mcp", "public.example", "public.example")] |
| studio/backend/tests/test_keyless_api_access_adversarial.py:580 | ip-literal-url | ("http://8.8.8.8:8888", "8.8.8.8:8888", False), |
| studio/backend/tests/test_lan_access_settings.py:365 | ip-literal-url | ("10.1.1.144", False, ["http://203.0.113.9:8888"]), |
| studio/backend/tests/test_lan_access_settings.py:378 | ip-literal-url | state = SimpleNamespace(server_url = "http://203.0.113.9:8888") |
| studio/backend/tests/test_lan_access_settings.py:411 | ip-literal-url | state = SimpleNamespace(server_url = "http://203.0.113.9:8888") |
| studio/backend/tests/test_lan_access_settings.py:464 | ip-literal-url | server_url = "http://203.0.113.9:8888", |
| studio/backend/tests/test_lan_access_settings.py:495 | ip-literal-url | assert status["urls"] == ["http://192.168.1.24:8888", "http://64.227.100.5:8888"] |
| studio/backend/tests/test_lan_access_settings.py:496 | ip-literal-url | assert status["public_urls"] == ["http://64.227.100.5:8888"] |
| studio/backend/tests/test_lan_share_host_resolution.py:238 | ip-literal-url | assert "http://203.0.113.9:8888" in capsys.readouterr().out |
| studio/backend/tests/test_provider_base_url_validation.py:410 | ip-literal-url | "http://100.100.100.200/latest/meta-data/", |
| studio/backend/tests/test_provider_base_url_validation.py:474 | ip-literal-url | assert 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:474 | ip-literal-url | assert 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:536 | ip-literal-url | assert sent == ["https://93.184.216.34/caf%C3%A9%201.png?q=%C3%A9"] |
| studio/backend/tests/test_sandbox_tools.py:1123 | ip-literal-url | + "a24('http://203.0.113.5/')" |
| studio/backend/tests/test_sandbox_tools.py:1141 | ip-literal-url | "import requests\nf = requests.get\nf = print\nf('http://203.0.113.5/')", |
| studio/backend/tests/test_sandbox_tools.py:1151 | ip-literal-url | "obj = make()\ngetattr(obj, 'get')('http://203.0.113.5/')", |
| studio/backend/tests/test_sandbox_tools.py:1174 | ip-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:1176 | ip-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)
| Advisory | Severity | Package | Summary |
|---|---|---|---|
| GHSA-rgw5-rvv9-x895 | high | brace-expansion@5.0.8 | brace-expansion: DoS via unbounded intermediate arrays, bypassing the CVE-2026-14257 mitigation |
| GHSA-3jxr-9vmj-r5cp | high | brace-expansion@1.1.14 | brace-expansion: DoS via exponential-time expansion of consecutive non-expanding {} groups |
| GHSA-mh99-v99m-4gvg | high | brace-expansion@1.1.14 | brace-expansion: DoS via unbounded expansion length causing an out-of-memory process crash |
| GHSA-rgw5-rvv9-x895 | high | brace-expansion@1.1.14 | brace-expansion: DoS via unbounded intermediate arrays, bypassing the CVE-2026-14257 mitigation |
| GHSA-73wf-gq98-2v4g | high | browserslist@4.28.2 | Browserslist: Uncaught crash / prototype write via untrusted browserslist-stats.json custom stats (normalizeStats) |
| GHSA-c83g-rgw3-j3cx | high | browserslist@4.28.2 | Browserslist: Unbounded memory growth (no cache eviction) via distinct query results, leading to eventual OOM |
| GHSA-5jgf-p345-68v8 | high | fast-uri@3.1.4 | fast-uri vulnerable to host confusion via skipped IDN canonicalization on scheme-relative references |
| GHSA-7p8r-x3mc-p8w7 | high | fast-uri@3.1.4 | fast-uri vulnerable to host confusion via backslash authority introducer |
| GHSA-f65p-4m7j-42xc | high | fast-uri@3.1.4 | fast-uri vulnerable to server-side request forgery via malformed IPv6 normalization |
| GHSA-fph4-wmhf-6fwf | high | fast-uri@3.1.4 | fast-uri vulnerable to server-side request forgery via repeated hostname percent-decoding |
| GHSA-jqff-g426-hqxp | high | fast-uri@3.1.4 | fast-uri vulnerable to host confusion via percent-encoded scheme normalization |
| GHSA-28wg-ghj8-5hjv | high | nanoid@5.1.11 | nanoid: non-secure generators can loop indefinitely with negative size |
| RUSTSEC-2026-0194 | high | quick-xml@0.37.5 | Quadratic run time when checking a start tag for duplicate attribute names |
| RUSTSEC-2026-0195 | high | quick-xml@0.37.5 | Unbounded namespace-declaration allocation in `NsReader` enables memory-exhaustion denial of service |
| RUSTSEC-2026-0194 | high | quick-xml@0.38.4 | Quadratic run time when checking a start tag for duplicate attribute names |
| RUSTSEC-2026-0195 | high | quick-xml@0.38.4 | Unbounded namespace-declaration allocation in `NsReader` enables memory-exhaustion denial of service |
| RUSTSEC-2026-0194 | high | quick-xml@0.39.2 | Quadratic run time when checking a start tag for duplicate attribute names |
| RUSTSEC-2026-0195 | high | quick-xml@0.39.2 | Unbounded namespace-declaration allocation in `NsReader` enables memory-exhaustion denial of service |
| GHSA-w5vr-8v7q-w6rv | moderate | baseline-browser-mapping@2.10.27 | baseline-browser-mapping process termination on invalid input causes denial of service |
| GHSA-54fx-42gc-7vw4 | moderate | hono@4.12.25 | Hono: Algorithmic Complexity DoS in Language Middleware |
| GHSA-8j4g-w8fx-2239 | moderate | hono@4.12.25 | Hono: ReDoS in CORS middleware via Access-Control-Request-Headers |
| GHSA-crvj-82cr-hjcx | moderate | hono@4.12.25 | Hono: Query parser reads parameters after the URL fragment, causing cache-key and proxy interpretation differentials |
| GHSA-f23p-vx2j-j53r | moderate | hono@4.12.25 | Hono: `memo()` retains SSR output across requests, leading to cross-user data disclosure |
| GHSA-g6gw-c38x-mqfc | moderate | hono@4.12.25 | Hono: Unbounded dot-notation nesting in `parseBody()` can cause memory exhaustion |
Workflows worth a look
- .github/workflows/ci-capacity.yml: pull_request_target
- .github/workflows/workflow-trigger-lint.yml: pull_request_target
By the numbers
| Stars | 76.8K |
|---|---|
| Forks | 7,054 |
| Contributors | 349 |
| Commits | 9,372 |
| Open issues | 802 |
| Open pull requests | 472 |
| Releases | 70 |
| Latest release | v0.1.815-beta |
| Licence | Apache-2.0 |
| Main language | Python |
| Project age | 2 years |
| Last push | Sep 27, 2026 |
| Tracked files | 6,271 |
| Lines of code | 2.7M |
| Checkout size | 133 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.
