[MODEL] Dangerous Gradle Code Pattern

Open 💬 0 comments Opened Jul 8, 2026 by JeffMcKnight

Preflight Checklist

  • [x] I have searched existing issues for similar behavior reports
  • [x] This report does NOT contain sensitive information (API keys, passwords, etc.)

Type of Behavior Issue

Other unexpected behavior

What You Asked Claude to Do

I asked "how to create .dmg containing the signed app"

Context:

  • Claude Code running in the IntelliJ plugin
  • OS: macOS 15.6.1

What Claude Actually Did

Claude added a createDmg task that creates a DMG package, as requested, and used Kotlin's java.io.File.deleteRecursively() to clean up the directory, which, when run, deleted several user apps on macOS (eg: IntelliJ, SourceTree, DiffMerge, PyCharm).

-----
Claude Code session log attached

2026-07-08-133454-local-command-caveatcaveat-the-messages-below.txt

-----
Zipped build.gradle.kts with the dangerous java.io.File.deleteRecursively() attached:

build.gradle.kts.zip

-----

Discussion from the Claude desktop app (outside the Claude Code session):
I found the cause in your log. Before explaining, let me verify the mechanism empirically:Confidence: 9/10

Found it. The deletion wasn't a Bash command — it was the createDmg Gradle task that Claude Code wrote and ran in your session. That's why the session's self-audit came up clean: Claude reviewed its shell commands, but the destructive code executed inside the Gradle JVM.

The mechanism (log lines ~638–677):

doLast {
    stagingDir.deleteRecursively()
    ...
    // ln -s /Applications  <staging>/Applications   ← standard DMG symlink
    // hdiutil create ...
    stagingDir.deleteRecursively()                    // ← the bomb
}

Kotlin's java.io.File.deleteRecursively() walks via File.isDirectory / File.listFiles(), and the legacy java.io.File API resolves symlinks — a symlink to a directory reports isDirectory == true and listFiles() returns the target's contents. So the trailing cleanup descended through the Applications symlink into the real /Applications and unlinked every entry your user account owns (IntelliJ, PyCharm, SourceTree, DiffMerge — anything drag-installed or brew-cask-installed). Root-owned/SIP-protected apps like Safari failed silently — deleteRecursively returns false rather than throwing — so the task exited 0 and the build "succeeded."

I just reproduced this on a JVM in my sandbox: a File-based recursive delete on a staging dir containing a symlink deleted the symlink target's contents, exactly this pattern.

Why every symptom matches:

  • Apps unlinked, not Trashed — File.delete() is unlink(2)
  • Nothing in ~/.zsh_history or the Bash transcript — it was JVM code, not shell
  • Timing: the background ./gradlew createDmg finished (exit 0) at 19:29 during your session; the deletion happened at the tail of that run. The DMG itself verified fine because hdiutil ran before the cleanup and correctly preserved the symlink inside the image
  • /Applications itself survived (the walk only empties it; it deletes the symlink, not the target dir)
  • Claude's later mdfind found Safari but not SourceTree — SIP-protected apps were undeletable

Immediate actions:

  1. Fix the task before anyone runs it again — either create the symlink after hdiutil doesn't need staging (i.e., delete the symlink explicitly with File(stagingDir, "Applications").delete() before the recursive cleanup), or better, use kotlin.io.path.deleteRecursively() (Kotlin 1.8+, Path-based), which explicitly does not follow symlinks — it exists precisely because of this java.io.File hazard. Docs: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io.path/delete-recursively.html
  2. Inventory what's gone: ls -la /Applications vs. brew list --cask and your Homebrew cache to rebuild the reinstall list.
  3. Check Time Machine / backups — app data under ~/Library survived (your log confirms plists and Application Support folders are intact), so reinstalled apps should pick up their settings.

Skeptical caveats: I can't fully rule out that the second, interrupted notarizeApp createDmg run (its leading deleteRecursively) was the actual trigger rather than the first run's trailing cleanup — but it doesn't matter; same code path either way. Also, the "deletion sound effect" is odd: unlink doesn't play Finder sounds, so that detail may have been coincidental or from something else — but the filesystem evidence is unambiguous. This is worth filing as a GitHub issue on anthropics/claude-code (https://github.com/anthropics/claude-code/issues); it's a genuinely dangerous code pattern for Claude to generate, and closely related incidents (#30700) exist without root cause identified.

Expected Behavior

Not sure what to propose. I guess Claude Code should warn the user when it generates code that calls recursive deletion API's?

Files Affected

Deleted:
* Several user-owned apps in the local /Applications directory

Permission Mode

Accept Edits was ON (auto-accepting changes)

Can You Reproduce This?

Haven't tried to reproduce

Steps to Reproduce

  1. Run the createDmg task from the attached build.gradle.kts file

(this is slightly speculative because I did not re-run the task that deleted my apps)

Claude Model

Sonnet

Relevant Conversation

See session log attached in the "What Claude Actually Did" section above.

Impact

Critical - Data loss or corrupted project

Claude Code Version

2.1.205 (Claude Code)

Platform

Anthropic API

Additional Context

I accepted Claude Code's suggestion that I allow it to remove the unsigned app from the Applications directory and deploy the signed app. In the future, probably best to not give Claude Code permissions to touch anything outside the project directory.

View original on GitHub ↗