# How to Find Tool Installation Paths with aqua which: A Complete Guide

> Find tool installation paths with aqua which. Learn how to resolve aqua-managed tool paths and fallback to system PATH for unmanaged binaries in this complete guide.

- Repository: [aquaproj/aqua](https://github.com/aquaproj/aqua)
- Tags: how-to-guide
- Published: 2026-02-25

---

**Use `aqua which <tool>` to resolve the absolute file system path of any tool managed by aqua, or fall back to system PATH for unmanaged binaries.**

The `aqua which` command is essential for debugging, scripting, and understanding how aqua organizes tools in your filesystem. Based on the aquaproj/aqua source code, this guide explains the complete resolution flow from CLI input to absolute path output.

## Understanding the aqua which Resolution Architecture

The command operates through three distinct layers that transform a tool name into an absolute filesystem path.

### CLI Layer (pkg/cli/which/command.go)

The entry point in [`pkg/cli/which/command.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/which/command.go) handles argument parsing and initializes the controller. The `action` function sets up the `config.Param` structure and invokes `InitializeWhichCommandController` before delegating to the core logic.

```go
func (i *command) action(ctx context.Context, args *Args) error {
    // Sets up param and logger
    ctrl := controller.InitializeWhichCommandController(ctx, i.r.Logger.Logger,
        param, http.DefaultClient, i.r.Runtime)
    
    which, err := ctrl.Which(ctx, logger, param, args.Command)
    // Outputs which.ExePath or which.Package.Version
}

```

### Controller Layer (pkg/controller/which/which.go)

The `Which` function in [`pkg/controller/which/which.go`](https://github.com/aquaproj/aqua/blob/main/pkg/controller/which/which.go) implements the search strategy. It scans configuration files in priority order: local [`aqua.yaml`](https://github.com/aquaproj/aqua/blob/main/aqua.yaml) files first, then global configurations, finally falling back to the system `PATH`.

```go
func (c *Controller) Which(ctx context.Context, logger *slog.Logger,
    param *config.Param, exeName string) (*FindResult, error) {
    
    // 1. Search local config files
    for _, cfgFilePath := range c.configFinder.Finds(param.CWD, "") {
        if fr, _ := c.findExecFile(ctx, logger, param, cfgFilePath, exeName); fr != nil {
            return fr, nil
        }
    }
    
    // 2. Search global config files
    // ...
    
    // 3. Fallback to $PATH
    if exePath := c.lookPath(c.osenv.Getenv("PATH"), exeName); exePath != "" {
        return &FindResult{ExePath: exePath}, nil
    }
    return nil, ErrCommandIsNotFound
}

```

### Domain Model (pkg/config/package.go)

Once a package is identified, `Package.ExePath` in [`pkg/config/package.go`](https://github.com/aquaproj/aqua/blob/main/pkg/config/package.go) constructs the absolute path. It computes the package directory using `AbsPkgPath` (which resolves to `$ROOT/pkgs/<type>/github.com/<owner>/<repo>/<version>`) and appends the specific file source.

```go
func (p *Package) ExePath(rootDir string, file *registry.File, rt *runtime.Runtime) (string, error) {
    pkgPath, err := p.AbsPkgPath(rootDir, rt)
    fileSrc, err := p.fileSrc(file, rt) // Resolves OS/Arch placeholders
    return filepath.Join(pkgPath, fileSrc), nil
}

```

## How aqua which Searches for Tools

The resolution follows a strict priority order to ensure deterministic behavior.

### Local Configuration Scanning

The controller first scans for [`aqua.yaml`](https://github.com/aquaproj/aqua/blob/main/aqua.yaml) files in the current working directory and parent directories using `configFinder.Finds`. For each configuration found, it calls `findExecFile` to check if the requested command matches any package defined in that file.

### Global Configuration Fallback

If no local configuration contains the tool, the search continues through global configuration files specified in the `AQUA_GLOBAL_CONFIG` environment variable or default global paths.

### System PATH Resolution

When aqua does not manage the tool, the command falls back to `lookPath`, which searches the system `PATH` environment variable. This allows `aqua which` to function as a universal tool locator, returning `/bin/ls` for system utilities while returning aqua-managed paths for registered tools.

## Building the Executable Path

Once a package match is found, the controller calls `getExePath` to assemble the final filesystem location.

```go
func (c *Controller) getExePath(findResult *FindResult) (string, error) {
    pkg := findResult.Package
    file := findResult.File
    exePath, err := pkg.ExePath(c.rootDir, file, c.runtime)
    if err != nil {
        return "", err
    }
    // Handle registry-defined symlinks
    if file.Link != "" {
        return filepath.Join(filepath.Dir(exePath), file.Link), nil
    }
    return exePath, nil
}

```

The `ExePath` method handles different package types (GitHub releases, Go modules, Cargo crates) by computing the appropriate subdirectory structure within the aqua root directory (typically `~/.aqua/pkgs/`).

## Practical Usage Examples

Use `aqua which` to verify installation locations and versions in your daily workflow.

```bash

# Locate a GitHub release tool

$ aqua which gh
/home/you/.aqua/pkgs/github_release/github.com/cli/cli/v2.4.0/gh

# Display the installed version

$ aqua which --version gh
v2.4.0

# Locate a system tool (falls back to PATH)

$ aqua which ls
/bin/ls

```

## Programmatic Access in Go

You can integrate aqua path resolution into your own Go applications using the controller package.

```go
import (
    "context"
    "log/slog"
    "os"

    "github.com/aquaproj/aqua/v2/pkg/config"
    "github.com/aquaproj/aqua/v2/pkg/controller/which"
)

func main() {
    ctx := context.Background()
    logger := slog.Default()
    
    // Initialize parameter with current working directory
    param := &config.Param{
        CWD: "/path/to/project",
        // Additional configuration as needed
    }
    
    // Initialize controller (simplified; actual initialization requires more dependencies)
    ctrl := which.NewController(/* dependencies */)
    
    result, err := ctrl.Which(ctx, logger, param, "gh")
    if err != nil {
        logger.Error("Tool not found", "error", err)
        os.Exit(1)
    }
    
    logger.Info("Executable path", "path", result.ExePath)
}

```

## Summary

- **`aqua which`** resolves the absolute filesystem path for any tool managed by aqua, falling back to system `PATH` for unmanaged binaries.
- The resolution traverses three layers: **CLI** ([`pkg/cli/which/command.go`](https://github.com/aquaproj/aqua/blob/main/pkg/cli/which/command.go)), **Controller** ([`pkg/controller/which/which.go`](https://github.com/aquaproj/aqua/blob/main/pkg/controller/which/which.go)), and **Domain Model** ([`pkg/config/package.go`](https://github.com/aquaproj/aqua/blob/main/pkg/config/package.go)).
- Search priority is: local [`aqua.yaml`](https://github.com/aquaproj/aqua/blob/main/aqua.yaml) configurations → global configurations → system `PATH`.
- The final path is constructed by `Package.ExePath`, which computes the installation directory based on package type (GitHub release, Go module, etc.).

## Frequently Asked Questions

### What happens if aqua which cannot find the tool?

If the tool is not defined in any local or global [`aqua.yaml`](https://github.com/aquaproj/aqua/blob/main/aqua.yaml) configuration, the command falls back to searching the system `PATH` using `exec.LookPath`. If the binary is not found in `PATH`, the command returns `ErrCommandIsNotFound` and exits with an error status.

### How does aqua which handle versioned binaries?

When a tool is managed by aqua, `aqua which` returns the path to the specific version installed in the aqua packages directory (typically `~/.aqua/pkgs/<type>/<source>/<version>/<binary>`). If you use the `--version` flag, the command outputs the version string from the package definition instead of the path.

### Can I use aqua which in shell scripts?

Yes, `aqua which` is designed for script integration. It outputs only the absolute path (or version with `--version`) to stdout, making it ideal for command substitution. For example: `GH_BIN=$(aqua which gh) && $GH_BIN repo view`. The command exits with a non-zero status if the tool is not found, enabling standard error handling in scripts.

### Where does aqua store installed tools?

Aqua stores tools in a structured directory under the aqua root (default `~/.aqua`). The path follows the pattern `pkgs/<package_type>/<source>/<version>/<file>`. For example, a GitHub release package appears at `pkgs/github_release/github.com/owner/repo/v1.0.0/binary`. The `aqua which` command computes this path dynamically based on the package definition in your registry.