Studio: remove AGENTS.md and CLAUDE.md from install artifacts (#7096)

* Studio: remove AGENTS.md from install artifacts

* Studio: prune CLAUDE.md from install artifacts

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix Studio instruction cleanup edge cases

* Trim Studio cleanup comments

* Make Studio cleanup safe on PowerShell 5.1

* Fix Studio cleanup ownership boundaries

* Simplify Windows link detection

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Daniel Han 2026-07-13 00:15:04 -07:00 committed by GitHub
commit 9e77c1e663
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 373 additions and 3 deletions

View file

@ -1,4 +1,4 @@
# i18n Contribution Instructions
# i18n Contribution Guide
- `locales/en.ts` is the complete baseline message file.
- Non-English locale files may be partial. Missing keys must fall back to English at runtime.
@ -9,4 +9,4 @@
- Keep product and technical names unchanged unless there is an established localized name, for example `Unsloth Studio`, `LoRA`, `GGUF`, and `Hugging Face`.
- Keep translation changes small and reviewable. Prefer separate commits for runtime changes, UI migration, and locale text.
- When adding user-facing Studio UI text, add the English message key first and add non-English overrides only when the translation is clear.
- Run `npx tsx src/i18n/check-parity.ts` before committing to ensure there are no shape mismatches or placeholder discrepancies in the non-English overlays.
- Run `npx tsx src/i18n/check-parity.ts` before committing to ensure there are no shape mismatches or placeholder discrepancies in the non-English overlays.

View file

@ -20,6 +20,7 @@ import re
import shutil
import site
import socket
import stat
import struct
import subprocess
import sys
@ -4385,6 +4386,48 @@ def copy_directory_contents(source_dir: Path, destination: Path) -> None:
shutil.copy2(item, target)
def _is_link_or_junction(path: Path) -> bool:
"""Return whether ``path`` redirects to another filesystem location."""
if os.name == "nt":
try:
attributes = getattr(path.lstat(), "st_file_attributes", 0)
except OSError:
return True
return bool(attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT)
try:
return path.is_symlink()
except OSError:
return True
def remove_agent_instruction_files(root: Path) -> int:
"""Best-effort removal inside a managed tree without following links."""
if _is_link_or_junction(root) or not root.is_dir():
return 0
removed = 0
for current_dir, dirnames, filenames in os.walk(root, topdown = True, followlinks = False):
current_path = Path(current_dir)
# followlinks=False still follows Windows junctions.
if current_path != root and _is_link_or_junction(current_path):
dirnames.clear()
continue
dirnames[:] = [
dirname for dirname in dirnames if not _is_link_or_junction(current_path / dirname)
]
for filename in sorted({"AGENTS.md", "CLAUDE.md"}.intersection(filenames)):
candidate = current_path / filename
try:
candidate.unlink()
except FileNotFoundError:
continue
except OSError as exc:
log(f"could not remove contributor-only instruction {candidate}: {exc}")
else:
removed += 1
return removed
def hydrate_source_tree(
source_ref: str,
install_dir: Path,
@ -4447,6 +4490,9 @@ def hydrate_source_tree(
"upstream source archive was missing required repo files: " + ", ".join(missing)
)
copy_directory_contents(source_root, install_dir)
removed = remove_agent_instruction_files(install_dir)
if removed:
log(f"removed {removed} contributor-only agent instruction file(s) from staged source")
except PrebuiltFallback:
raise
except Exception as exc:
@ -6824,6 +6870,7 @@ def install_prebuilt(
override_has_rocm: bool = False,
override_rocm_gfx: str | None = None,
force_cpu: bool = False,
instruction_cleanup_root: Path | None = None,
) -> None:
host = detect_host()
host = _apply_host_overrides(
@ -6836,8 +6883,15 @@ def install_prebuilt(
host, published_repo, published_release_tag, force_cpu = force_cpu
)
choice: AssetChoice | None = None
cleanup_root = install_dir if instruction_cleanup_root is None else instruction_cleanup_root
try:
with install_lock(install_lock_path(install_dir)):
if (install_dir / "UNSLOTH_PREBUILT_INFO.json").is_file():
removed = remove_agent_instruction_files(cleanup_root)
if removed:
log(
f"removed {removed} contributor-only agent instruction file(s) from install"
)
if install_dir.exists():
log(
f"existing llama.cpp install detected at {install_dir}; validating staged prebuilt update before replacement"
@ -7168,14 +7222,16 @@ def main() -> int:
# Install path only: route status logs to stdout (see _LOG_TO_STDOUT note).
global _LOG_TO_STDOUT
_LOG_TO_STDOUT = True
install_arg = Path(args.install_dir).expanduser()
install_prebuilt(
install_dir = Path(args.install_dir).expanduser().resolve(),
install_dir = install_arg.resolve(),
llama_tag = args.llama_tag,
published_repo = args.published_repo,
published_release_tag = args.published_release_tag or "",
override_has_rocm = args.has_rocm,
override_rocm_gfx = args.rocm_gfx,
force_cpu = args.cpu_fallback,
instruction_cleanup_root = install_arg.absolute(),
)
return EXIT_SUCCESS

View file

@ -203,6 +203,31 @@ function New-UnslothTemporaryFile {
return Get-Item -LiteralPath $tempPath
}
function Remove-AgentInstructionFiles {
param([string[]]$Roots)
foreach ($root in $Roots) {
if (-not $root) { continue }
$item = Get-Item -LiteralPath $root -Force -ErrorAction SilentlyContinue
if (-not $item -or -not $item.PSIsContainer) { continue }
if ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { continue }
$pending = New-Object System.Collections.Stack
$pending.Push($item)
while ($pending.Count -gt 0) {
$current = $pending.Pop()
foreach ($child in @(Get-ChildItem -LiteralPath $current.FullName -Force -ErrorAction SilentlyContinue)) {
if ($child.PSIsContainer) {
if (-not ($child.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
$pending.Push($child)
}
} elseif ($child.Name -in @("AGENTS.md", "CLAUDE.md")) {
Remove-Item -LiteralPath $child.FullName -Force -ErrorAction SilentlyContinue
}
}
}
}
}
function Get-InstalledLlamaPrebuiltRelease {
param([string]$InstallDir)
@ -2314,6 +2339,8 @@ if ((Test-Path $OxcValidatorDir) -and $NodeSource -ne "skip" -and (Get-Command n
substep "OXC validator runtime skipped (no npm found); code validation degrades until Node is available" "Yellow"
}
Remove-AgentInstructionFiles -Roots @($FrontendDir, $OxcValidatorDir)
# ==========================================================================
# PHASE 3: Python environment + dependencies
# ==========================================================================
@ -3259,6 +3286,7 @@ if ($LocalLlamaCppSrc) {
if ($LASTEXITCODE -ne 0) {
substep "Could not create directory junction; copying instead..." "Yellow"
Copy-Item -Recurse -LiteralPath $ResolvedLocal -Destination $LlamaCppDir
Remove-AgentInstructionFiles -Roots @($LlamaCppDir)
}
Write-Host ""
step "llama.cpp" "linked local directory: $ResolvedLocal"
@ -4024,6 +4052,16 @@ if ($LocalLlamaCppLinked) {
}
}
$llamaCppItem = Get-Item -LiteralPath $LlamaCppDir -Force -ErrorAction SilentlyContinue
$llamaCppIsLink = $llamaCppItem -and ($llamaCppItem.Attributes -band [System.IO.FileAttributes]::ReparsePoint)
if (-not $llamaCppIsLink -and (
-not $StudioHomeIsCustom -or
(Test-Path -LiteralPath (Join-Path $LlamaCppDir $StudioOwnedMarker) -PathType Leaf) -or
(Test-StudioOwnedAdoptable $LlamaCppDir)
)) {
Remove-AgentInstructionFiles -Roots @($LlamaCppDir)
}
# ─────────────────────────────────────────────
# Footer
# ─────────────────────────────────────────────

View file

@ -74,6 +74,16 @@ verbose_substep() {
return 0
}
_remove_agent_instruction_files() {
local _root
for _root in "$@"; do
[ -d "$_root" ] || continue
[ -L "$_root" ] && continue
find "$_root" -type f \( -name 'AGENTS.md' -o -name 'CLAUDE.md' \) \
-exec rm -f {} + 2>/dev/null || true
done
}
# ── Corporate-mirror / proxy escape hatch for the frontend npm/bun install (#6491) ──
# studio/frontend/.npmrc pins registry=https://registry.npmjs.org/ as a supply-chain
# lock. A project-level pin overrides a corporate user's ~/.npmrc proxy, so the install
@ -847,6 +857,8 @@ elif [ -d "$_OXC_DIR" ] && [ "${NODE_SOURCE:-}" != skip ]; then
substep "OXC validator runtime skipped (no npm found); code validation degrades until Node is available" "$C_WARN"
fi
_remove_agent_instruction_files "$SCRIPT_DIR/frontend" "$_OXC_DIR"
# ── Python venv + deps ──
[ -d "$REPO_ROOT/.venv" ] && rm -rf "$REPO_ROOT/.venv"
@ -1919,6 +1931,14 @@ if [ "$_LLAMA_CPP_DEGRADED" = true ] \
fi
fi
if [ ! -L "$LLAMA_CPP_DIR" ] && {
[ "$_STUDIO_HOME_IS_CUSTOM" != true ] ||
[ -f "$LLAMA_CPP_DIR/$_STUDIO_OWNED_MARKER" ] ||
_studio_owned_adoptable "$LLAMA_CPP_DIR"
}; then
_remove_agent_instruction_files "$LLAMA_CPP_DIR"
fi
# ── Footer ──
if [ "$_LLAMA_ONLY" = "1" ]; then
echo ""

View file

@ -3,6 +3,7 @@ import importlib.util
import io
import json
import os
import subprocess
import sys
import tarfile
import zipfile
@ -30,6 +31,7 @@ AssetChoice = INSTALL_LLAMA_PREBUILT.AssetChoice
ApprovedArtifactHash = INSTALL_LLAMA_PREBUILT.ApprovedArtifactHash
ApprovedReleaseChecksums = INSTALL_LLAMA_PREBUILT.ApprovedReleaseChecksums
hydrate_source_tree = INSTALL_LLAMA_PREBUILT.hydrate_source_tree
remove_agent_instruction_files = INSTALL_LLAMA_PREBUILT.remove_agent_instruction_files
validate_prebuilt_choice = INSTALL_LLAMA_PREBUILT.validate_prebuilt_choice
activate_install_tree = INSTALL_LLAMA_PREBUILT.activate_install_tree
activate_staged_dir = INSTALL_LLAMA_PREBUILT.activate_staged_dir
@ -203,6 +205,178 @@ def test_extract_archive_rejects_zip_symlink_entry(tmp_path: Path):
extract_archive(archive_path, tmp_path / "extract")
def test_remove_agent_instruction_files_does_not_follow_links(tmp_path: Path):
managed = tmp_path / "managed"
nested = managed / "nested"
external = tmp_path / "external"
nested.mkdir(parents = True)
external.mkdir()
(managed / "AGENTS.md").write_text("managed root", encoding = "utf-8")
(nested / "AGENTS.md").write_text("managed nested", encoding = "utf-8")
(managed / "CLAUDE.md").write_text("managed Claude root", encoding = "utf-8")
(nested / "CLAUDE.md").write_text("managed Claude nested", encoding = "utf-8")
(external / "AGENTS.md").write_text("user owned", encoding = "utf-8")
(external / "CLAUDE.md").write_text("user-owned Claude", encoding = "utf-8")
try:
(managed / "external-link").symlink_to(external, target_is_directory = True)
linked_root = tmp_path / "linked-root"
linked_root.symlink_to(external, target_is_directory = True)
except OSError as exc:
pytest.skip(f"directory symlinks unavailable: {exc}")
assert remove_agent_instruction_files(managed) == 4
assert not list(managed.rglob("AGENTS.md"))
assert not list(managed.rglob("CLAUDE.md"))
assert (external / "AGENTS.md").read_text(encoding = "utf-8") == "user owned"
assert (external / "CLAUDE.md").read_text(encoding = "utf-8") == "user-owned Claude"
assert remove_agent_instruction_files(linked_root) == 0
assert (external / "AGENTS.md").exists()
assert (external / "CLAUDE.md").exists()
@pytest.mark.skipif(os.name != "nt", reason = "Windows junction behavior")
def test_remove_agent_instruction_files_does_not_follow_windows_junctions(tmp_path: Path):
managed = tmp_path / "managed"
external = tmp_path / "external"
managed.mkdir()
external.mkdir()
(external / "AGENTS.md").write_text("user owned", encoding = "utf-8")
(external / "CLAUDE.md").write_text("user-owned Claude", encoding = "utf-8")
nested_junction = managed / "external-junction"
root_junction = tmp_path / "linked-root"
for junction in (nested_junction, root_junction):
result = subprocess.run(
["cmd", "/d", "/c", "mklink", "/J", str(junction), str(external)],
capture_output = True,
text = True,
check = False,
)
if result.returncode != 0:
pytest.skip(f"directory junctions unavailable: {result.stderr or result.stdout}")
assert remove_agent_instruction_files(managed) == 0
assert remove_agent_instruction_files(root_junction) == 0
assert (external / "AGENTS.md").read_text(encoding = "utf-8") == "user owned"
assert (external / "CLAUDE.md").read_text(encoding = "utf-8") == "user-owned Claude"
def test_remove_agent_instruction_files_prunes_linklike_directories(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
managed = tmp_path / "managed"
simulated_junction = managed / "simulated-junction"
simulated_junction.mkdir(parents = True)
agents = simulated_junction / "AGENTS.md"
claude = simulated_junction / "CLAUDE.md"
agents.write_text("external instructions", encoding = "utf-8")
claude.write_text("external Claude instructions", encoding = "utf-8")
real_is_link_or_junction = INSTALL_LLAMA_PREBUILT._is_link_or_junction
monkeypatch.setattr(
INSTALL_LLAMA_PREBUILT,
"_is_link_or_junction",
lambda path: path == simulated_junction or real_is_link_or_junction(path),
)
assert remove_agent_instruction_files(managed) == 0
assert agents.exists()
assert claude.exists()
def test_remove_agent_instruction_files_continues_after_unlink_error(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
):
managed = tmp_path / "managed"
managed.mkdir()
blocked = managed / "AGENTS.md"
removable = managed / "CLAUDE.md"
blocked.write_text("blocked", encoding = "utf-8")
removable.write_text("remove me", encoding = "utf-8")
real_unlink = Path.unlink
def selective_unlink(path: Path, *args, **kwargs):
if path == blocked:
raise PermissionError(errno.EACCES, "Access is denied", str(path))
return real_unlink(path, *args, **kwargs)
monkeypatch.setattr(Path, "unlink", selective_unlink)
assert remove_agent_instruction_files(managed) == 1
assert blocked.exists()
assert not removable.exists()
captured = capsys.readouterr()
assert "could not remove contributor-only instruction" in captured.out + captured.err
def test_main_resolves_linked_install_path_and_preserves_cleanup_root(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
target = tmp_path / "target"
linked_root = tmp_path / "linked-root"
target.mkdir()
try:
linked_root.symlink_to(target, target_is_directory = True)
except OSError as exc:
pytest.skip(f"directory symlinks unavailable: {exc}")
received = {}
monkeypatch.setattr(
sys,
"argv",
["install_llama_prebuilt.py", "--install-dir", str(linked_root)],
)
monkeypatch.setattr(
INSTALL_LLAMA_PREBUILT,
"install_prebuilt",
lambda **kwargs: received.update(kwargs),
)
monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "_LOG_TO_STDOUT", False)
assert INSTALL_LLAMA_PREBUILT.main() == 0
assert received["install_dir"] == target.resolve()
assert received["instruction_cleanup_root"] == linked_root.absolute()
assert received["instruction_cleanup_root"].is_symlink()
def test_install_prebuilt_uses_explicit_instruction_cleanup_root(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
install_dir = tmp_path / "target"
linked_root = tmp_path / "linked-root"
install_dir.mkdir()
(install_dir / "UNSLOTH_PREBUILT_INFO.json").write_text("{}", encoding = "utf-8")
try:
linked_root.symlink_to(install_dir, target_is_directory = True)
except OSError as exc:
pytest.skip(f"directory symlinks unavailable: {exc}")
cleanup_roots = []
monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "detect_host", linux_host)
monkeypatch.setattr(
INSTALL_LLAMA_PREBUILT,
"remove_agent_instruction_files",
lambda root: cleanup_roots.append(root) or 0,
)
monkeypatch.setattr(
INSTALL_LLAMA_PREBUILT,
"resolve_simple_install_release_plans",
lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("stop after cleanup")),
)
with pytest.raises(RuntimeError, match = "stop after cleanup"):
install_prebuilt(
install_dir.resolve(),
"latest",
"unslothai/llama.cpp",
"",
instruction_cleanup_root = linked_root.absolute(),
)
assert cleanup_roots == [linked_root.absolute()]
def test_hydrate_source_tree_extracts_upstream_archive_contents(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
@ -224,6 +398,26 @@ def test_hydrate_source_tree_extracts_upstream_archive_contents(
f"llama.cpp-{upstream_tag}/gguf-py/gguf/__init__.py",
b"__all__ = []\n",
)
add_bytes_to_tar(
archive,
f"llama.cpp-{upstream_tag}/AGENTS.md",
b"upstream contributor instructions\n",
)
add_bytes_to_tar(
archive,
f"llama.cpp-{upstream_tag}/examples/AGENTS.md",
b"nested contributor instructions\n",
)
add_bytes_to_tar(
archive,
f"llama.cpp-{upstream_tag}/CLAUDE.md",
b"Claude contributor instructions\n",
)
add_bytes_to_tar(
archive,
f"llama.cpp-{upstream_tag}/examples/CLAUDE.md",
b"nested Claude contributor instructions\n",
)
source_urls = set(INSTALL_LLAMA_PREBUILT.upstream_source_archive_urls(upstream_tag))
@ -244,6 +438,8 @@ def test_hydrate_source_tree_extracts_upstream_archive_contents(
assert (install_dir / "convert_hf_to_gguf.py").exists()
assert (install_dir / "gguf-py" / "gguf" / "__init__.py").exists()
assert not (install_dir / f"llama.cpp-{upstream_tag}").exists()
assert not list(install_dir.rglob("AGENTS.md"))
assert not list(install_dir.rglob("CLAUDE.md"))
def test_release_asset_download_url():
@ -644,6 +840,29 @@ def test_activate_install_tree_restores_existing_install_after_activation_failur
assert "restored previous install from rollback path" in output
def test_activate_install_tree_preserves_symlink_to_resolved_target(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
install_dir = tmp_path / "target"
linked_root = tmp_path / "linked-root"
staging_dir = tmp_path / "staging"
install_dir.mkdir()
staging_dir.mkdir()
(install_dir / "old.txt").write_text("old", encoding = "utf-8")
(staging_dir / "new.txt").write_text("new", encoding = "utf-8")
try:
linked_root.symlink_to(install_dir, target_is_directory = True)
except OSError as exc:
pytest.skip(f"directory symlinks unavailable: {exc}")
monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "confirm_install_tree", lambda *_args: None)
activate_install_tree(staging_dir, linked_root.resolve(), linux_host())
assert linked_root.is_symlink()
assert (linked_root / "new.txt").read_text(encoding = "utf-8") == "new"
assert not (linked_root / "old.txt").exists()
def test_activate_install_tree_cleans_all_paths_when_rollback_restore_fails(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
):
@ -2007,6 +2226,14 @@ def test_install_prebuilt_skips_download_when_existing_install_matches(
approved_checksums = checksums,
prebuilt_fallback_used = False,
)
(install_dir / "AGENTS.md").write_text("old root instructions", encoding = "utf-8")
nested_agents = install_dir / "examples" / "AGENTS.md"
nested_agents.parent.mkdir()
nested_agents.write_text("old nested instructions", encoding = "utf-8")
(install_dir / "CLAUDE.md").write_text("old Claude instructions", encoding = "utf-8")
(nested_agents.parent / "CLAUDE.md").write_text(
"old nested Claude instructions", encoding = "utf-8"
)
monkeypatch.setattr(INSTALL_LLAMA_PREBUILT, "detect_host", lambda: host)
monkeypatch.setattr(
@ -2026,6 +2253,35 @@ def test_install_prebuilt_skips_download_when_existing_install_matches(
)
install_prebuilt(install_dir, "latest", "unslothai/llama.cpp", "")
assert not list(install_dir.rglob("AGENTS.md"))
assert not list(install_dir.rglob("CLAUDE.md"))
def test_setup_scripts_prune_agent_files_without_shipping_a_repo_copy():
setup_sh = (PACKAGE_ROOT / "studio" / "setup.sh").read_text(encoding = "utf-8")
setup_ps1 = (PACKAGE_ROOT / "studio" / "setup.ps1").read_text(encoding = "utf-8")
assert '_remove_agent_instruction_files "$SCRIPT_DIR/frontend" "$_OXC_DIR"' in setup_sh
assert '_remove_agent_instruction_files "$LLAMA_CPP_DIR"' in setup_sh
assert "-name 'CLAUDE.md'" in setup_sh
assert 'if [ ! -L "$LLAMA_CPP_DIR" ] && {' in setup_sh
assert '${_LOCAL_LLAMA_CPP_LINKED:-false}" != true' not in setup_sh
assert "$LLAMA_CPP_DIR/$_STUDIO_OWNED_MARKER" in setup_sh
assert '_studio_owned_adoptable "$LLAMA_CPP_DIR"' in setup_sh
assert "Remove-AgentInstructionFiles -Roots @($FrontendDir, $OxcValidatorDir)" in setup_ps1
assert '"CLAUDE.md"' in setup_ps1
assert '-Include "AGENTS.md", "CLAUDE.md"' not in setup_ps1
assert '$child.Name -in @("AGENTS.md", "CLAUDE.md")' in setup_ps1
assert "$llamaCppIsLink" in setup_ps1
assert "if (-not $LocalLlamaCppLinked)" not in setup_ps1
assert "Join-Path $LlamaCppDir $StudioOwnedMarker" in setup_ps1
assert "Test-StudioOwnedAdoptable $LlamaCppDir" in setup_ps1
assert (
"Copy-Item -Recurse -LiteralPath $ResolvedLocal -Destination $LlamaCppDir\n"
" Remove-AgentInstructionFiles -Roots @($LlamaCppDir)"
) in setup_ps1
assert not (PACKAGE_ROOT / "studio" / "frontend" / "src" / "i18n" / "AGENTS.md").exists()
assert (PACKAGE_ROOT / "studio" / "frontend" / "src" / "i18n" / "README.md").is_file()
def test_install_prebuilt_does_not_skip_unhealthy_existing_install(