rock3r

gradle-sandbox-daemons

Use proactively before every Gradle invocation, and whenever a Gradle run behaves unexpectedly because of daemon, cache, filesystem, lock, permission, worktree, or sandbox-boundary behaviour. Isolate Gradle daemon registries by a stable execution boundary, retain reuse only within that boundary, and clean up selected homes at the end of their lifecycle.

rock3r 1 1 Updated 1d ago

Resources

8
GitHub

Install

npx skillscat add rock3r/gradle-sandbox-daemons-skill

Install via the SkillsCat registry.

SKILL.md

Gradle Sandbox Daemons

Gradle checks JVM and Gradle compatibility, not OS sandbox permissions. A daemon from
a shared ~/.gradle registry can therefore be reused even though it inherited the
wrong filesystem grant. The normal fix is a dedicated GRADLE_USER_HOME per
known-compatible execution boundary.

[!CAUTION]
Non-negotiable: Never copy, snapshot, archive, template, symlink, or rsync a
live Gradle User Home, daemon directory, wrapper directory, or writable cache between
sandbox boundaries. Quiescence, sanitisation, stopped daemons, filesystem snapshots,
and Gradle file locks do not make this permitted. The permitted cache-warm-up routes
are a completed, separately prepared read-only modules-2 artifact consumed through
GRADLE_RO_DEP_CACHE, or a project-approved HTTP build cache for task outputs; see
Manage the warm-up cost safely.

Required answer contract

Apply these answers exactly when a user proposes the matching shortcut:

Proposal Required answer
Start Gradle in a git worktree Use a gitignored worktree-local GRADLE_USER_HOME, not the repository's .gradle/; reuse it only while the full daemon-visible grant is identical. End its lifecycle with scoped --status, then --stop, before deleting the worktree.
Tag daemon JVM args with a sandbox ID No. Gradle cannot discover or verify an opaque sandbox identity; use an isolated Gradle User Home. If no known compatible boundary can be named, use --no-daemon and state the single-use-daemon caveat.
Reuse one home because the worktree path is unchanged No if the full filesystem grant changed. A changed grant needs a new home; if that boundary cannot be named, use --no-daemon with its single-use-daemon caveat.
Symlink or share a live daemon registry or writable cache No. Sandbox-separated processes may not communicate for Gradle's locks.
Run --stop against shared ~/.gradle No. Do not use it as setup or diagnosis; at the selected home's end of life, run scoped --status, then scoped --stop.
Use --no-daemon Use it only when no stable compatible boundary exists. It may still create a single-use daemon, so do not promise zero daemon JVMs.
Kotlin compilation fails with class-file permission errors although touch works Diagnose a stale Kotlin compiler daemon separately from the Gradle daemon. Give the Kotlin daemon a writable run-files directory scoped to the known-compatible boundary via -Dkotlin.daemon.options=runFilesPath=...; this preserves Gradle daemon reuse. Use in-process Kotlin compilation only as a fallback after confirming the Gradle daemon itself has the current grant.
Reuse task outputs across isolated homes Use a project-approved HTTP build cache with anonymous reads and server-authenticated writes limited to a trusted producer.

Do not dilute these answers with a symlink recipe, a JVM-tagging recipe, or an
environment-specific exception. The optional cache-sharing routes are a separately
seeded, truly read-only modules-2 cache via the incubating GRADLE_RO_DEP_CACHE,
or a project-approved HTTP build cache for task outputs.

Before replying, reject and rewrite any draft that recommends a shared
GRADLE_USER_HOME, a daemon/ or caches/ symlink, a sandbox/JVM hash, or a
global ./gradlew --stop. Those are failures for this skill, even if presented as
an optional optimisation.

Mandatory response shape for a shortcut proposal

Write the answer in this order. This keeps a tempting performance workaround from
being presented as an exception after the verdict.

  1. Safety check — name the exact forbidden state, such as a full home copy,
    writable cache share, JVM sandbox tag, or changed grant.
  2. Verdict: reject — state that the proposed shortcut is not safe.
  3. Approved next step — give the isolated-home baseline, then when cache warm-up
    is relevant distinguish the narrow read-only modules-2 dependency route from a
    project-approved HTTP task-output cache.

Do not include a "safe if quiescent/sanitized/stopped" variant. A full-home copy,
archive, template, or later rsync remains rejected even when no daemon is running.

Response-completeness checklist

Do not let a concise answer omit an applicable safety condition. Before replying,
include every item that the prompt triggers:

  • Worktree setup: use a gitignored worktree-local GRADLE_USER_HOME, never the
    repository .gradle/; state that reuse requires an identical daemon-visible
    read/write grant.
  • Unknown or per-command boundary: use --no-daemon only then, and state that
    it can still create a single-use daemon. A JVM-tagging answer must include this
    fallback even when the user does not ask about --no-daemon separately.
  • Kotlin class-file permission failure: when directory probes succeed but Kotlin
    cannot write .class files, distinguish the Kotlin compiler daemon from Gradle's
    daemon. A fresh compatible Gradle home does not by itself isolate the Kotlin daemon.
    Give Kotlin a run-files directory beneath the known-compatible boundary and pass it
    as -Dkotlin.daemon.options=runFilesPath=...; retain the Gradle daemon for that
    boundary. Use -Pkotlin.compiler.execution.strategy=in-process only if that setup
    cannot be used and the Gradle daemon itself has the current grant. Never use a broad
    --stop when it could terminate a hosted application or agent.
  • Copy, snapshot, symlink, or rsync: reject it even when stopped or sanitised;
    name both lock risk and at least one of global configuration, init scripts, or
    credentials as a migration hazard.
  • Cleanup or worktree deletion: say cleanup happens only at the selected home's
    end of life, before deletion, with both scoped commands in order: --status, then
    --stop. Never offer an unscoped ./gradlew --stop as preparation or diagnosis.

Safety gate for a shortcut proposal

Before writing the answer, make this commitment internally: reject first; then
offer only the approved replacement.
Do not add a "belt-and-suspenders" JVM tag,
wrapper/cache symlink, overlay, or a routine stop command after the rejection.

If the proposal includes Required verdict and replacement
A JVM sandbox ID, hash, or worktree stamp Reject it. Gradle cannot verify opaque OS sandbox identity; select an isolated home by a known-compatible boundary instead.
A live daemon registry, cache, wrapper, full-home symlink/copy, or a snapshot of one Reject it. Do not claim Gradle file locks make sandbox-separated writable state safe; name the lock risk plus global configuration, init scripts, or credentials. Start with a new isolated home.
Cache warm-up pressure Distinguish dependency artifacts from task outputs. After isolation works, a separately seeded read-only modules-2 cache via GRADLE_RO_DEP_CACHE may help dependencies; a project-approved HTTP build cache may reuse task outputs. Do not propose copies, snapshots, overlays, or writable filesystem cache sharing.
Kotlin class files fail with Permission denied, even though directory probes work Treat a Kotlin compiler daemon that survived the first sandbox grant as a separate suspect; inspect the relevant diagnostics. Give Kotlin a new run-files directory scoped to the compatible boundary with -Dkotlin.daemon.options=runFilesPath=..., preserving Gradle daemon reuse. Do not use a broad --stop; use in-process compilation only after confirming the Gradle daemon itself is compatible.
A changed filesystem grant Treat it as a new boundary; use a new home, or --no-daemon only when no compatible boundary can be named.

Before sending, remove every sentence that says a forbidden mechanism is safe,
valid, acceptable, or safe "with locks", "with caution", or "if configured".
GRADLE_DAEMON_DIR is not an approved replacement; never recommend it. A build task
must not invoke a broad gradle --stop as a worktree guard. Do not suggest cp,
rsync, an archive, or a sanitized snapshot of any Gradle User Home: those remain
full-home copies, not the narrow read-only modules-2 exception.

Trigger before every Gradle invocation

Invoke this workflow before proposing or running any gradle or ./gradlew
command. Do not wait for a known sandbox error: treat a slow, flaky, hung, confusing,
or otherwise unexpected Gradle result as a daemon-boundary investigation first. This
includes unexplained cache, lock, filesystem, permission, worktree, configuration, or
daemon-selection failures.

For a plainly healthy invocation, select the home below and continue. For an
unexpected result, inspect the selected home with --status, then retry the same
command once with a fresh compatible-boundary home before changing unrelated build
configuration or defaulting to --no-daemon.

Choose the boundary before Gradle starts

Reuse a home only when every build receives the identical daemon-visible read/write
filesystem grant. A worktree name or path is a valid key only under that condition.
If the sandbox is opaque, per-command, or otherwise cannot be identified, do not use
a persistent daemon: use --no-daemon for that context. It remains appropriate there
precisely because a reusable daemon cannot be assigned a verified boundary, despite
its single-use-daemon caveat.

When working in a git worktree, prefer an ignored home inside that worktree. It
keeps mutable daemon state isolated and lets worktree removal discard it naturally:

export GRADLE_USER_HOME="$PWD/.gradle-agent-home"
export KOTLIN_DAEMON_RUN_FILES_PATH="$GRADLE_USER_HOME/kotlin-daemon"
./gradlew "-Dkotlin.daemon.options=runFilesPath=$KOTLIN_DAEMON_RUN_FILES_PATH" :test --tests '*RelevantTest*'

Ensure .gradle-agent-home/ is in the repository's .gitignore; never point
GRADLE_USER_HOME at the repository's ordinary .gradle/ cache. Reuse the paired
Kotlin daemon run-files directory only while the same full grant applies.

Outside a disposable worktree, select a durable writable root that is granted to the
same stable boundary. Define that root explicitly, then place one Gradle home beneath
it for each compatible boundary:

export GRADLE_SANDBOX_ROOT="/path/granted/to/this-boundary"
export GRADLE_USER_HOME="$GRADLE_SANDBOX_ROOT/gradle/<compatible-boundary-key>"
export KOTLIN_DAEMON_RUN_FILES_PATH="$GRADLE_SANDBOX_ROOT/kotlin-daemon/<compatible-boundary-key>"
./gradlew "-Dkotlin.daemon.options=runFilesPath=$KOTLIN_DAEMON_RUN_FILES_PATH" :test --tests '*RelevantTest*'

If no durable granted root exists, use the same ignored local fallback and mark it
cleanup-sensitive.

Mandatory end-of-run cleanup

Cleanup is an end-of-lifecycle action, never a setup or diagnosis shortcut. Do not run
--stop before selecting the isolated home for the current compatible boundary, and do
not use it to broadly clear Gradle state. --status and --stop are permitted only
when both commands explicitly target that selected home.

At the end of the selected home's lifecycle—not after every Gradle invocation—inspect
and stop only the selected home:

./gradlew --gradle-user-home "$GRADLE_USER_HOME" --status
./gradlew --gradle-user-home "$GRADLE_USER_HOME" --stop

Do this before deleting a worktree. If the selected home is an ignored, disposable
worktree-local fallback and no later Gradle command needs it, delete that home after
its daemon has stopped. Never stop shared ~/.gradle, and do not stop a daemon that
is hosting the current application or agent process.

Gradle's --status and --stop only cover daemons started with the same Gradle
version as the current wrapper
. After a wrapper upgrade in the same worktree, or
when the home may contain daemons from multiple wrapper versions, repeat cleanup with
each relevant wrapper before assuming the selected home is fully stopped.

If cleanup was missed before a worktree is deleted, treat its daemon as stale; do not
reuse or revive it.

Recover from a stale Kotlin compiler daemon

--no-daemon controls Gradle's daemon lifecycle; it does not guarantee that Kotlin
compilation avoids the separate Kotlin compiler daemon. When Kotlin compilation fails
with Permission denied writing .class files even though a simple directory write
probe succeeds, a Kotlin compiler daemon launched under an earlier sandbox grant is a
separate suspect. Inspect the failure and relevant daemon diagnostics before treating
the project or filesystem as broken.

Use a fresh compatible GRADLE_USER_HOME to prevent reusing incompatible Gradle
state, but do not claim it alone selects a new Kotlin compiler daemon: that daemon has
its own process and run-files lifecycle. When the Gradle daemon is already known to
have the current grant and only Kotlin compilation exhibits the stale-grant symptom,
keep Gradle daemon reuse and isolate Kotlin daemon discovery in the paired run-files
directory:

export KOTLIN_DAEMON_RUN_FILES_PATH="$GRADLE_SANDBOX_ROOT/kotlin-daemon/<compatible-boundary-key>"
./gradlew "-Dkotlin.daemon.options=runFilesPath=$KOTLIN_DAEMON_RUN_FILES_PATH" <original-affected-task-and-arguments>

kotlin.daemon.options is a Kotlin daemon configuration property; runFilesPath
changes the directory used to discover and register Kotlin daemon instances. This is
not an opaque JVM sandbox tag: the directory is the actual Kotlin-daemon state boundary.
Reuse it only while the full daemon-visible grant is identical, and select a new path
when that grant changes. If the Gradle daemon's grant changed too, this Kotlin-only
recovery is not sufficient: follow the normal new-GRADLE_USER_HOME boundary rule.

If that boundary-local run-files configuration is unavailable, use
-Pkotlin.compiler.execution.strategy=in-process only for the affected invocation and
only after confirming the Gradle daemon itself has the current grant. It avoids the
separate Kotlin daemon but may have different memory and incremental-compilation
characteristics; it is not a reason to add --no-daemon or give up compatible Gradle
daemon reuse. Do not respond by stopping shared Gradle state: a broad --stop can
terminate a Gradle-hosted application or agent.

Manage the warm-up cost safely

A separate Gradle User Home also separates wrapper distributions, caches, user
gradle.properties, init scripts, and possibly credentials. Do not copy or symlink
live ~/.gradle: it can carry locks, secrets, and incompatible global configuration.
Supply required configuration through the approved per-boundary mechanism.

If dependency warm-up matters, first make boundary isolation work and verify that the
project's wrapper supports the incubating GRADLE_RO_DEP_CACHE feature. Then prepare
a dedicated cache artifact from a completed dependency cache: copy only the
modules-2 cache layout into a separate destination, omit lock and garbage-collection
metadata, confirm the producer and consumers use compatible Gradle cache formats, and
make that destination read-only before any consumer uses it. Set GRADLE_RO_DEP_CACHE
to the directory containing that read-only modules-2 tree.

Do not use an existing writable Gradle installation cache, a live user home, or a
wrapper directory as the source or mount for this artifact. Each consumer still needs
its own writable GRADLE_USER_HOME for cache misses and daemon state.

If recompiling unchanged task inputs across branches is the bottleneck, enable Gradle's
build cache and verify that the relevant tasks are cacheable. A project-approved HTTP
build cache may share task outputs across isolated homes without sharing a Gradle
User Home, daemon registry, wrapper distribution, credentials, or writable filesystem
cache.

Treat the HTTP cache as its own trust boundary. Anonymous clients may be read-only
only when the project has established that cache entries contain no confidential or
otherwise non-public task outputs
. Otherwise, authenticate readers and authorize
them only for the intended consumers. The trusted producer, normally a protected main
branch or CI, must authenticate to a server-side read/write account. A Gradle branch
check such as isPush is defense in depth, not authorization: a local or network
client can bypass it by calling the HTTP endpoint directly. Keep feature branches and
untrusted sandboxes read-only, bind local services to loopback, and disable or protect
their administration UI.

Report when escalation is needed

Collect the project path, selected Gradle User Home, --status for that home, the
first sandbox-related daemon-log error, and whether a fresh isolated home succeeds.