Grape Dependency Management

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Groovy 6.0.0 (released 2026-09-22), run on JDK 26 and checked on 2026-10-14. Grape (the Groovy Adaptable Packaging Engine) is the dependency manager that lets a single Groovy file declare the libraries it needs: at compile time the @Grab annotation resolves the named artifact and its transitive dependencies from a Maven repository, downloads the jars into a local cache and adds them to the class loader before the rest of the script is resolved. A script can then be shipped on its own, which is why Grape is the usual answer to "how do I use a library from a script". It is meant for scripts, tools and experiments. A compiled application or a Gradle or Maven project declares its dependencies in its build, described on Build Tools: Gradle, Maven & Ant.

Every example was run with groovy 6.0.0 from a scratch directory, and every JVM was started with -Dgrape.root=<scratch>/grape-root (through JAVA_OPTS) so that the downloaded jars went to a throwaway directory and not to ~/.groovy/grapes. The artifacts came from Maven Central. Output lines that the logging system wrote to standard error and that carry no information (the WARNING lines the JVM prints for a mutated final field by the grape command, for instance) are left out; the ones that matter are discussed where they appear.

In Groovy 6.0 Grape is split in modules: the facade (groovy.grape.Grape and the annotations) is in the core jar, the Apache Ivy engine is groovy-grape-ivy (it was part of the core before 6.0) and a second engine based on Maven Resolver is groovy-grape-maven, new and incubating in 6.0. Both jars are in the lib directory of the standard distribution. The engines are covered in The two engines.

Your first @Grab

The shorthand form takes group:module:version. Put the annotation on an import, which is the form the documentation recommends, because an annotated import is evaluated before the imported class is looked up:

@Grab('org.apache.commons:commons-text:1.13.0')
import org.apache.commons.text.WordUtils

def zones = ['north wing', 'south wing', 'seedling room']
zones.each { println WordUtils.capitalizeFully(it) }
assert WordUtils.capitalizeFully('north wing') == 'North Wing'
North Wing
South Wing
Seedling Room

The first run downloads commons-text and its dependency commons-lang3; later runs find them in the cache and start without touching the network. The long form names the parts as attributes, which is useful when a value comes with a classifier or other options:

@Grab(group = 'org.apache.commons', module = 'commons-text', version = '1.13.0')
import org.apache.commons.text.similarity.LevenshteinDistance

def distance = LevenshteinDistance.defaultInstance.apply('tomato', 'tomatoes')
println "distance = $distance"
assert distance == 2
distance = 2

Where the annotation can go

@Grab can annotate an import, a class, a method, a constructor, a field, a parameter or a local variable. The effect is the same wherever it is: the resolution happens when the script or class is compiled, and a matching call is added to the static initializer of the containing class (or of the script class for a script element). Annotations on the same element can be repeated; the @Grapes container, needed in the very first versions of Groovy, is optional today. A method and a local variable:

class Reporter {
    @Grab('org.apache.commons:commons-text:1.13.0')
    static String title(String s) {
        org.apache.commons.text.WordUtils.capitalizeFully(s)
    }
}

@Grab('commons-codec:commons-codec:1.17.1')
var digest = org.apache.commons.codec.digest.DigestUtils.sha256Hex('zone-1')

println Reporter.title('north wing')
println digest.take(16)
North Wing
88a58816fa1c5b56

The annotations have source retention only, so nothing about them is visible to reflection at run time.

The annotation family

Annotation Purpose

@Grab

One dependency: group, module, version, plus classifier, ext, type, conf, transitive, force, changing and initClass; or the value shorthand group:module:version.

@Grapes

A container for several @Grab annotations. Optional since repeated @Grab annotations became legal.

@GrabResolver

Adds a repository: name and root (or value for the root only), m2Compatible (default true).

@GrabExclude

Removes one transitive dependency from the resolution: group and module, or the shorthand group:module.

@GrabConfig

Controls how the grab itself behaves: systemClassLoader, systemProperties, initContextClassLoader, autoDownload and disableChecksums.

The fields come from the annotation types of the 6.0.0 distribution. @GrabExclude with a dependency that the code really uses is shown on AST Transformations: Logging, Safety, Concurrency & Directives, together with the other @Grab annotations among the compiler directives.

Versions, the cache and the resolver chain

The version is a literal such as 1.13.0 or a range in Ivy notation: [1.16,1.18) is "1.16 up to but excluding 1.18", [2.2.1,) is "2.2.1 or any greater version". A range resolved against a cache that already holds a matching version picks from what is available, so a run can differ between machines. Pin an exact version in anything that must repeat:

@Grab('commons-codec:commons-codec:[1.16,1.18)')
import org.apache.commons.codec.binary.Hex

println Hex.encodeHexString('A1'.bytes)
4131

With the default engine the artifact was found as version 1.17.1, as grape list showed afterwards. The Ivy engine looks in three places, in this order, as its verbose log lists them: cachedGrapes (the Grape cache), localm2 (the local Maven repository, ~/.m2/repository) and ibiblio (Maven Central). The local Maven repository is a convenient source, but it also means a half-populated Maven cache can fool a resolution; see Hardening.

The cache lives under the Grape root, ~/.groovy by default, in a directory named after the engine: grapes for the Ivy engine and grapesM2 for the Maven one. The grape.root system property moves the root, which is how the runs on this page were isolated. With -Dgrape.root=<scratch>/grape-root the first script above left this layout (shortened):

grape-root/grapes/org.apache.commons/commons-text/ivy-1.13.0.xml
grape-root/grapes/org.apache.commons/commons-text/jars/commons-text-1.13.0.jar
grape-root/grapes/org.apache.commons/commons-lang3/jars/commons-lang3-3.17.0.jar

Sharing one cache between a team’s machines, putting it on a CI cache volume, or deleting it to force a fresh download are all done through this directory. Deleting one module directory is enough to make Grape fetch that module again.

Additional repositories with @GrabResolver

Maven Central is searched by default. @GrabResolver adds another repository, such as a company’s Nexus or Artifactory server or a file system directory. This example uses a tiny repository created for the page: a one-class jar and its POM in Maven layout under a local directory (the directory was in the scratch area; it appears below as /path/to/local-repo):

@GrabResolver(name = 'greenhouse-local', root = 'file:///path/to/local-repo')
@Grab('com.example:greenhouse-sensors:1.0.0')
import greenhouse.Sensors

println Sensors.describe('north wing', 0.27)
assert Sensors.describe('north wing', 0.27) == 'north wing: soil moisture 0.27'
north wing: soil moisture 0.27

The jar and the POM were found at com/example/greenhouse-sensors/1.0.0/ below the root, the path a Maven repository uses. The Ivy engine, while setting a custom resolver up, wrote a problems summary with the line unknown resolver greenhouse-local to standard error on the first run, and then resolved the artifact and ran the script anyway; the same script with the Maven engine printed nothing of the kind. Treat it as noise when the script ends with the expected result, and as a real problem when the resolution reports that the module could not be found. A resolver whose name collides with a built-in one (central, for instance) is a real error, as the page on the AST transformations shows. Pick a distinct name.

The root should be an https:// URL. A plain http:// root is reported by the hardening checks described below because anything fetched over it can be read or changed on the way.

Classifiers, excludes and transitive dependencies

A Maven classifier picks a variant of the same coordinates, such as a tests or sources jar. Name it as the fourth element of the shorthand or in the classifier attribute:

@Grab(group = 'org.apache.commons', module = 'commons-collections4', version = '4.4', classifier = 'tests')
import org.apache.commons.collections4.AbstractObjectTest

println AbstractObjectTest.protectionDomain.codeSource.location.path.split('/').last()
commons-collections4-4.4-tests.jar

A dependency brings its own dependencies along unless transitive = false says otherwise. To see exactly which jars an artifact brings, ask the facade, which returns the jar locations:

import groovy.grape.Grape

def args = [group: 'org.apache.commons', module: 'commons-text', version: '1.13.0']
def all = Grape.instance.resolve([:], args)
def alone = Grape.instance.resolve([:], args + [transitive: false])
println all*.toString().collect { it.split('/').last() }
println alone*.toString().collect { it.split('/').last() }
[commons-text-1.13.0.jar, commons-lang3-3.17.0.jar]
[commons-text-1.13.0.jar]

@GrabExclude is the finer tool: it drops one transitive module and keeps the rest, which is the way to stay on a version of a library that the application already provides, or to remove a module that would clash. The attributes are group and module, or @GrabExclude('xml-apis:xml-apis') for the shorthand. The effect on the classpath is the effect of removing the jar, so code that needs the class fails with NoClassDefFoundError, as the page linked in the previous section demonstrates.

JDBC drivers and @GrabConfig

@GrabConfig configures the grab. Its most famous option is systemClassLoader = true, which attaches the jars to the system class loader instead of the loader of the script. It exists because of how JDBC finds drivers: DriverManager only offers a driver to code that can see the driver’s class loader. A driver grabbed into the script’s class loader is invisible to groovy.sql.Sql, which is loaded by the Groovy runtime. With H2 (an embedded database, see Databases with Groovy SQL):

@GrabConfig(systemClassLoader = true)
@Grab('com.h2database:h2:2.3.232')
import groovy.sql.Sql

def sql = Sql.newInstance('jdbc:h2:mem:greenhouse')
sql.execute('create table zone (name varchar(20), soil decimal(4,2))')
sql.execute("insert into zone values ('north wing', 0.27)")
sql.eachRow('select name, soil from zone') { println "${it.name} ${it.soil}" }
sql.close()
north wing 0.27

The same script without the @GrabConfig line fails, in this setup, with:

java.sql.SQLException: No suitable driver found for jdbc:h2:mem:greenhouse
	at java.sql/java.sql.DriverManager.getConnection(DriverManager.java:638)
	at groovy.sql.Sql.newInstance(Sql.java:353)

Calling DriverManager.getConnection directly from the script did work without it, because the script’s own loader could see the driver. The other options of @GrabConfig:

Option Effect

systemClassLoader

Adds the jars to the system class loader. Needed for JDBC drivers and for libraries that look themselves up through it.

systemProperties

Sets system properties for the grab, as key=value strings.

initContextClassLoader

Also sets the thread context class loader to the loader that received the jars.

autoDownload

false restricts resolution to what the cache holds (Ivy engine only, see below).

disableChecksums

Skips the verification of the downloaded checksums. Turn it on only to get past a broken mirror, never as a default.

Grape from code

The annotation is the compile-time front end of a facade that can also be called directly. Grape.grab takes a map with group, module, version and optionally classifier, conf (Ivy configuration or Maven scope), force, changing and transitive. There are synonyms for the keys (groupId, organisation, organization, org for the group; artifactId and artifact for the module; version, revision and rev), and giving two synonyms at once is a runtime error. Because the call happens at run time, the classes are not known to the compiler: reach them with Class.forName or loadClass, or split the script so that the code using them is evaluated later.

import groovy.grape.Grape

Grape.grab(group: 'org.apache.commons', module: 'commons-text', version: '1.13.0')

def wordUtils = Class.forName('org.apache.commons.text.WordUtils')
println wordUtils.capitalizeFully('seedling room')

// the map synonyms: groupId/artifactId/revision
Grape.grab(groupId: 'commons-codec', artifactId: 'commons-codec', revision: '1.17.1')
println Class.forName('org.apache.commons.codec.binary.Hex').encodeHexString('A1'.bytes)

// a missing artifact: silence the exception with noExceptions
def ok = Grape.grab(noExceptions: true, group: 'com.example', module: 'no-such-module', version: '9.9.9')
println "noExceptions result: $ok"
try {
    Grape.grab(group: 'com.example', module: 'no-such-module', version: '9.9.9')
} catch (RuntimeException e) {
    println "failed: ${e.message.readLines().first()}"
}
Seedling Room
4131
noExceptions result: null
failed: Error grabbing Grapes -- [unresolved dependency: com.example#no-such-module;9.9.9: not found]

The jars go to a class loader that must be a RootLoader or a GroovyClassLoader, found in this order: the one passed as classLoader:, the loader of the object passed as refObject:, and the loader of the class that calls grab. A script run by the groovy command has one. In an application that embeds Groovy, create a GroovyClassLoader, pass it, and load the classes through it:

import groovy.grape.Grape

def loader = new GroovyClassLoader()
Grape.grab(classLoader: loader, group: 'org.apache.commons', module: 'commons-text', version: '1.13.0')
def cls = loader.loadClass('org.apache.commons.text.WordUtils')
println cls.capitalizeFully('seedling room')

try {
    Class.forName('org.apache.commons.text.WordUtils')
} catch (ClassNotFoundException e) {
    println "not visible to the default loader: ${e.message}"
}
Seedling Room
not visible to the default loader: org.apache.commons.text.WordUtils

The second map argument and its options (validate, noExceptions) belong to the two-map form Grape.grab(args, dependency1, dependency2, ...); a call with a single map uses it as both. Embedding is the subject of Integrating Groovy into Java Applications.

Grape in groovysh

In the shell there are no annotations on imports to rely on, since every input is evaluated on its own. The documentation suggests the method call; groovysh also has a /grab command that wraps it (see groovysh & groovyconsole). Both were run by piping the lines to groovysh -q -C false:

groovy> groovy.grape.Grape.grab(group:'org.apache.commons', module:'commons-text', version:'1.13.0')
groovy> org.apache.commons.text.WordUtils.capitalizeFully('north wing')
North Wing
groovy> /exit
groovy> /grab org.apache.commons:commons-text:1.13.0
groovy> org.apache.commons.text.WordUtils.capitalizeFully('south wing')
South Wing
groovy> /exit

The groovyConsole has no such command; start it with a script that holds the @Grab annotation, or call Grape.grab in the first lines of the script window.

The grape command

The grape executable in $GROOVY_HOME/bin inspects and manages the cache. Its commands in 6.0.0:

Command Purpose

grape install <group> [<module>] [<version>] [<classifier>]

Downloads the module and its dependencies into the cache. Without a version the latest one is used.

grape list

Lists the modules in the cache with their versions.

grape resolve [-adhisv] <group> <module> <version>

Prints the jar locations of the module and its transitive dependencies, ready for use in another tool: -s as a Unix shell CLASSPATH assignment, -d for a Windows batch file, -a as Ant pathelement entries.

grape uninstall <group> <module> <version>

Removes the module’s jar from the cache, non-transitively.

The global options are -q, -w, -i, -V and -d (log level 0 to 4), -D to define a system property, -r to add a resolver for an install, -h and -v. Since 6.0 install also accepts the coordinate shorthands of @Grab. The three forms below are equivalent; the third one is the Ivy notation group#module;version, and a classifier and an extension are written group:module:version:classifier@ext (this form is from the 6.0 release notes):

$ grape -q install org.apache.commons commons-text 1.13.0
$ grape -q install org.apache.commons:commons-text:1.13.0
$ grape -q install 'org.apache.commons#commons-text;1.13.0'

A session with a fresh cache (the cache path is shortened to <grape.root>):

$ grape -q install commons-codec:commons-codec:1.17.1
$ grape -q install org.apache.commons commons-text 1.13.0
$ grape -q install org.apache.commons:commons-collections4:4.4:tests
$ grape list

commons-codec commons-codec  [1.17.1]
org.apache.commons commons-text  [1.13.0]
org.apache.commons commons-lang3  [3.17.0]
org.apache.commons commons-collections4  [4.4]
org.apache.commons commons-parent  [48, 71]
org.apache apache  [21, 32]
org.junit junit-bom  [5.11.0-M2]

7 Grape modules cached
9 Grape module versions cached
$ grape resolve -s org.apache.commons commons-text 1.13.0
export CLASSPATH=<grape.root>/grapes/org.apache.commons/commons-text/jars/commons-text-1.13.0.jar:<grape.root>/grapes/org.apache.commons/commons-lang3/jars/commons-lang3-3.17.0.jar
$ grape -q uninstall commons-codec commons-codec 1.17.1
Deleting commons-codec-1.17.1.jar

grape install is the way to warm a cache in advance, for instance in the image build of a container that later runs scripts without network access. grape resolve turns a Grape dependency into a plain classpath for Java, Ant or a shell script. uninstall deletes the jar but the module still shows in list, with an empty version list; remove the module directory by hand when you want it to disappear. Without -q the Ivy engine prints its resolution messages at INFO level, which is noisy in scripts.

The two engines

Groovy 6.0 ships two implementations of the Grape engine behind the same annotations and facade (incubating in 6.0):

Aspect GrapeIvy (default) GrapeMaven (new)

Backend

Apache Ivy

Maven Resolver

Engine class

groovy.grape.ivy.GrapeIvy

groovy.grape.maven.GrapeMaven

Module

groovy-grape-ivy

groovy-grape-maven

Cache

~/.groovy/grapes (Ivy layout)

~/.groovy/grapesM2 (Maven layout)

Configuration file

~/.groovy/grapeConfig.xml (Ivy settings)

none; use @GrabResolver or Grape.addResolver

conf:

Ivy configurations; lists such as ['default','optional']

a single Maven scope; default maps to compile

@GrabConfig(autoDownload = false)

honoured, cache-only resolution

not honoured

Version *

Ivy latest.default

Maven LATEST

When both engines are on the classpath, GrapeIvy wins. Select the other one with the groovy.grape.impl system property, on the command line or globally in JAVA_OPTS. A script that prints which engine served it, run twice against the same Grape root:

@Grab('org.apache.commons:commons-text:1.13.0')
import groovy.grape.Grape
import org.apache.commons.text.WordUtils

println "engine: ${Grape.instance.class.name}"
println WordUtils.capitalizeFully('north wing')
$ groovy engine.groovy
engine: groovy.grape.ivy.GrapeIvy
North Wing

$ groovy -Dgroovy.grape.impl=groovy.grape.maven.GrapeMaven engine.groovy
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
engine: groovy.grape.maven.GrapeMaven
North Wing

Afterwards the Grape root held both caches side by side: grapes from the first run and grapesM2 (with org/apache/commons/commons-text/1.13.0/commons-text-1.13.0.jar below it) from the second. The SLF4J lines come from Maven Resolver, which looks for a logging binding; they are harmless. Both engines honour grape.root. The local file repository script from the section on @GrabResolver also ran unchanged with the Maven engine.

A value of groovy.grape.impl that names a class that does not exist leaves no engine to resolve the annotation, and the script fails at compile time with unable to resolve class for the imports it was meant to supply. A custom engine is a class implementing groovy.grape.GrapeEngine, registered in META-INF/services/groovy.grape.GrapeEngine of a jar on the classpath and selected with the same property; if it is the only one on the classpath it is found without it.

Moving a script between engines

Most scripts work unchanged. The differences to check, all from the table above, are conf: lists (rework them to a single scope, or stay with Ivy), a ~/.groovy/grapeConfig.xml with custom Ivy settings (re-register the repositories with @GrabResolver), and autoDownload = false. The last one is easy to observe. With the artifact missing from the cache, the Ivy engine honours the flag and refuses to download, and the Maven engine ignores it and downloads:

@GrabConfig(autoDownload = false)
@Grab('commons-beanutils:commons-beanutils:1.9.4')
import org.apache.commons.beanutils.BeanUtils

println BeanUtils.class.name
$ groovy offline-miss.groovy                      (Ivy engine, empty cache)
...
General error during conversion: Error grabbing Grapes -- [unresolved dependency: commons-beanutils#commons-beanutils;1.9.4: not found]

$ groovy -Dgroovy.grape.impl=groovy.grape.maven.GrapeMaven offline-miss.groovy   (Maven engine, empty cache)
org.apache.commons.beanutils.BeanUtils

With the artifact already in the cache the same flag lets the script run with no network, which was the case for a copy of the first script in this page:

@GrabConfig(autoDownload = false)
@Grab('org.apache.commons:commons-text:1.13.0')
import org.apache.commons.text.WordUtils

println WordUtils.capitalizeFully('cached copy')
Cached Copy

For offline use on the Maven engine, warm the cache with grape install while online and then point @GrabResolver at a local-only repository, as the release notes recommend, or stay on the Ivy engine.

Proxies and logging

Behind a proxy, pass the standard JVM properties, on the command line or for every run in JAVA_OPTS:

groovy -Dhttp.proxyHost=proxy.internal -Dhttp.proxyPort=8080 report.groovy
JAVA_OPTS="-Dhttp.proxyHost=proxy.internal -Dhttp.proxyPort=8080"

This was not run here (it needs a proxy); the host names are placeholders. For https repositories the matching https.proxyHost and https.proxyPort properties apply as for any JVM program. Credentials for a repository that needs them do not belong in the script or in an annotation; they belong in the build tool or in the environment, and a script that needs a private repository is better turned into a Gradle or Maven project.

To see what Grape is doing, the Ivy engine accepts -Dgroovy.grape.report.downloads=true, which prints one line for each resolve and download to standard error. Against an empty cache:

Resolving dependency: org.apache.commons#commons-text;1.13.0 {default=[default]}
Preparing to download artifact org.apache.commons#commons-text;1.13.0!commons-text.jar
Preparing to download artifact org.apache.commons#commons-lang3;3.17.0!commons-lang3.jar
Downloaded 909 Kbytes in 7ms:
  [SUCCESSFUL ] org.apache.commons#commons-text;1.13.0!commons-text.jar (1ms)
  [SUCCESSFUL ] org.apache.commons#commons-lang3;3.17.0!commons-lang3.jar (1ms)

(The download here came from the local Maven repository, hence the short times.) Since 6.0 Groovy routes its diagnostics through the JDK’s platform logging, so the detailed output is controlled with a logging.properties file: Groovy reads ~/.groovy/logging.properties or the file named by -Djava.util.logging.config.file. A file that raises the Ivy engine’s logger to FINE:

handlers = java.util.logging.ConsoleHandler
java.util.logging.ConsoleHandler.level = ALL
groovy.grape.ivy.level = FINE

printed, among many lines, the resolver chain and each step of the resolution:

FINE: 	default resolver: downloadGrapes
FINE: 	-- 4 resolvers:
FINE: 	downloadGrapes [chain] [cachedGrapes, localm2, ibiblio]
FINE: == resolving dependencies caller#all-caller;working98->org.apache.commons#commons-text;1.13.0 [default->default]

The levels are INFO, FINE and FINEST for increasing detail, and the grape command’s -q, -w, -i, -V and -d flags adjust the Ivy level for one invocation. The Maven engine ignores groovy.grape.report.downloads; its switch is -Dgroovy.grape.debug=true, which prints lines such as:

[GrapeDebug] resolving root=org.apache.commons:commons-text:jar:1.13.0 transitive=true
[GrapeDebug] resolved org.apache.commons:commons-lang3:jar:3.17.0 -> <grape.root>/grapesM2/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar

Hardening

Grape downloads code and puts it on the class path of a program that is about to run it. Whatever the repository serves, it executes with the rights of the script. Three habits matter more than any option: pin exact versions, use only repositories you control or trust, and prefer https://. Groovy 6.0 added several checks of its own:

  • A coordinate part that contains a path separator or a .. segment is rejected before it can reach the cache layout:

    • Grab: invalid value of '../../etc' for module: should only contain - . _ a-z A-Z 0-9

  • A resolver root that uses a plaintext protocol is reported. The policy is chosen with -Dgroovy.grape.insecureProtocolPolicy: warn (the default) logs it and adds the resolver, fail refuses it, ignore skips the check. Loopback hosts are exempt under every policy.

  • The Maven engine now fails the resolution on a checksum mismatch, as the Ivy engine always did. Artifacts published with no checksum still resolve. @GrabConfig(disableChecksums = true) or -Dgroovy.grape.disableChecksums turn the verification off completely, which defeats the check.

  • The strict resolvers for the local Maven repository and for the Grape cache reject half-populated entries (a POM with no jar, a corrupt jar), so a damaged entry is fetched again; a relaxed configuration restores the old behaviour.

  • A corrupt or truncated jar no longer aborts the whole @Grab: extension-method scanning skips it with a warning.

  • grape uninstall deletes only inside the module cache, checked on canonical paths.

The first two bullets were run. The coordinate check and the two policies against Grape.addResolver:

import groovy.grape.Grape

try {
    Grape.grab(group: 'org.apache.commons', module: '../../etc', version: '1.0')
} catch (RuntimeException e) {
    println "rejected: ${e.message.readLines().first()}"
}

Grape.addResolver(name: 'plain', root: 'http://repo.example.test/maven2/')
Grape.addResolver(name: 'loopback', root: 'http://localhost:8081/maven2/')
println 'resolvers added'
$ groovy hardening.groovy
rejected: Grab: invalid value of '../../etc' for module: should only contain - . _ a-z A-Z 0-9
resolvers added

$ groovy -Dgroovy.grape.insecureProtocolPolicy=fail hardening.groovy
rejected: Grab: invalid value of '../../etc' for module: should only contain - . _ a-z A-Z 0-9
java.lang.RuntimeException: Grape resolver 'plain' uses the plaintext root 'http://repo.example.test/maven2/' and was rejected because -Dgroovy.grape.insecureProtocolPolicy=fail is set. ...
	at groovy.grape.Grape.checkResolverRootProtocol(Grape.java:451)

With the default warn policy the plaintext resolver was added after a log line from checkResolverRootProtocol, and the loopback one never triggered the check. The host is a reserved test name; the script never connected to it. Checksum and cache handling are described in the release notes and were not exercised here.

Finally, -Dgroovy.grape.enable=false switches @Grab off. A script that depends on one fails to compile, with the import it was meant to provide reported as unable to resolve class. That is the switch for an environment (a locked-down CI job or a server that compiles user scripts) where code must not fetch anything. Compile-time restrictions for scripts that come from outside are described on Compilation Customizers & Secure Scripting.

Where the book is outdated

The first edition of the reference book (Groovy 1.0, 2007) predates Grape altogether: Grape arrived in Groovy 1.6 (2009). A script of the book’s era placed jars on the command line with -cp, in $GROOVY_HOME/lib or in ~/.groovy/lib, and that still works but is the opposite of a self-contained script. What differs now: the @Grab annotation on an import, the shorthand coordinates and the grape command, the grape.root property to move the cache, and, new in 6.0, the split of the Ivy engine into its own module, the second engine based on Maven Resolver, platform logging, the shorthands of grape install and the hardening checks. Documentation written for 2.x or 3.x still describes the Ivy engine only, the groovy.grape.report.downloads property as the logging switch and ~/.groovy/grapeConfig.xml as the way to configure it; those remain true for GrapeIvy but not for the new engine. Examples that grab a http:// repository are also dated, since Maven Central itself is https only and the plaintext check now warns. The collected list of changes is on What’s New & Migrating from Older Versions.