> ## Documentation Index
> Fetch the complete documentation index at: https://docs.devin.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Template library

> Copy-paste blueprints for common languages, private registries, and enterprise infrastructure.

Copy-paste blueprints for common languages and use cases. Each template is self-contained. Combine them to build your full configuration.

For a full breakdown of every field, see the [Blueprint reference](/onboard-devin/environment/blueprint-reference).

<Info>
  **Secrets:** Templates reference secrets via `$SECRET_NAME`. Configure these in the **Secrets** tab within each blueprint editor before using a template. Never hardcode credentials in your blueprint.
</Info>

***

## Quick start

Minimal blueprints for the most common setups. Copy one, paste it into the blueprint editor, and you're done.

<AccordionGroup>
  <Accordion title="Node.js project">
    ```yaml theme={null}
    initialize: |
      npm install -g pnpm

    maintenance: |
      pnpm install

    knowledge:
      - name: lint
        contents: |
          Run `pnpm lint` to check for errors.
      - name: test
        contents: |
          Run `pnpm test` for the full suite.
    ```
  </Accordion>

  <Accordion title="Python project">
    ```yaml theme={null}
    initialize: |
      curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance: |
      uv sync

    knowledge:
      - name: lint
        contents: |
          Run `uv run ruff check .` to lint.
      - name: test
        contents: |
          Run `uv run pytest` for the full suite.
    ```
  </Accordion>

  <Accordion title="Full-stack (Node + Python)">
    ```yaml theme={null}
    initialize:
      - name: Install pnpm
        run: npm install -g pnpm
      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance:
      - name: Frontend deps
        run: (cd frontend && pnpm install)
      - name: Backend deps
        run: (cd backend && uv sync)

    knowledge:
      - name: structure
        contents: |
          - `frontend/` — React app (pnpm)
          - `backend/` — Python API (uv)
      - name: test
        contents: |
          Frontend: cd frontend && pnpm test
          Backend: cd backend && uv run pytest
    ```
  </Accordion>
</AccordionGroup>

***

## Repository blueprints

Per-repo build steps, dependency management, and knowledge entries. Set these in **Settings > Environment > Blueprints > \[your repo]**.

### Python

<Tabs>
  <Tab title="uv (recommended)">
    Recommended setup for Python projects using [uv](https://docs.astral.sh/uv/) for dependency management.

    ```yaml theme={null}
    initialize: |
      curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance: |
      uv sync

    knowledge:
      - name: lint
        contents: |
          uv run ruff check .

          Auto-fix:
          uv run ruff check --fix .

      - name: test
        contents: |
          uv run pytest

      - name: build
        contents: |
          uv run python -m build
    ```
  </Tab>

  <Tab title="pip + venv">
    Traditional Python setup with pip and venv. Use when your project relies on `requirements.txt`.

    ```yaml theme={null}
    initialize: |
      python3 -m venv .venv

    maintenance: |
      source .venv/bin/activate
      pip install -r requirements.txt

    knowledge:
      - name: lint
        contents: |
          source .venv/bin/activate
          flake8 .

      - name: test
        contents: |
          source .venv/bin/activate
          pytest

      - name: build
        contents: |
          source .venv/bin/activate
          python -m build
    ```
  </Tab>
</Tabs>

### Node.js

<Tabs>
  <Tab title="npm">
    Standard Node.js setup with npm.

    ```yaml theme={null}
    initialize: |
      nvm install 20
      nvm use 20

    maintenance: |
      npm install

    knowledge:
      - name: lint
        contents: |
          npx eslint .

      - name: test
        contents: |
          npm test

      - name: build
        contents: |
          npm run build
    ```

    <Info>
      Use `npm install` (not `npm ci`) in `maintenance`. It does an incremental update, while `npm ci` deletes `node_modules` and reinstalls from scratch every session.
    </Info>
  </Tab>

  <Tab title="pnpm">
    For projects using pnpm.

    ```yaml theme={null}
    initialize: |
      npm install -g pnpm

    maintenance: |
      pnpm install --frozen-lockfile

    knowledge:
      - name: lint
        contents: |
          pnpm lint

      - name: test
        contents: |
          pnpm test

      - name: build
        contents: |
          pnpm build
    ```
  </Tab>
</Tabs>

### Go

Standard Go setup with modules.

```yaml theme={null}
initialize: |
  GO_VERSION=1.23.5
  ARCH=$(dpkg --print-architecture)
  curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \
    | sudo tar -xz -C /usr/local
  echo 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"' \
    | sudo tee /etc/profile.d/golang.sh > /dev/null

  go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

maintenance: |
  go mod download

knowledge:
  - name: lint
    contents: |
      golangci-lint run

  - name: test
    contents: |
      go test ./...

  - name: build
    contents: |
      go build ./...
```

### Java

<Tabs>
  <Tab title="Gradle">
    Java setup with Gradle.

    <Info>
      JDK 17 is **pre-installed** on Devin's base image. Skip the JDK install step if the default OpenJDK 17 is sufficient.
    </Info>

    ```yaml theme={null}
    initialize:
      - name: Install JDK 17
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Gradle
        run: |
          GRADLE_VERSION=8.12
          curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
            -o /tmp/gradle.zip
          sudo unzip -qo /tmp/gradle.zip -d /opt
          sudo ln -sf /opt/gradle-${GRADLE_VERSION}/bin/gradle /usr/local/bin/gradle
          rm /tmp/gradle.zip

    maintenance: |
      ./gradlew dependencies

    knowledge:
      - name: lint
        contents: |
          ./gradlew check

      - name: test
        contents: |
          ./gradlew test

      - name: build
        contents: |
          ./gradlew build
    ```
  </Tab>

  <Tab title="Maven">
    Java setup with Maven.

    <Info>
      JDK 17 is **pre-installed** on Devin's base image. Skip the JDK install step if the default OpenJDK 17 is sufficient.
    </Info>

    ```yaml theme={null}
    initialize:
      - name: Install JDK 17
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Maven
        run: |
          MAVEN_VERSION=3.9.9
          curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
            | sudo tar -xz -C /opt
          sudo ln -sf /opt/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/local/bin/mvn

    maintenance: |
      mvn dependency:resolve

    knowledge:
      - name: test
        contents: |
          mvn test

      - name: build
        contents: |
          mvn package
    ```
  </Tab>
</Tabs>

### Ruby on Rails

Rails setup with PostgreSQL.

```yaml theme={null}
initialize:
  - name: Install Ruby 3.3
    run: |
      sudo apt-get update -qq
      sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ruby-full libpq-dev postgresql-client

maintenance: |
  bundle install
  rails db:migrate

knowledge:
  - name: lint
    contents: |
      bundle exec rubocop

  - name: test
    contents: |
      bundle exec rspec

  - name: build
    contents: |
      rails assets:precompile
```

### Rust

Standard Rust setup with cargo.

<Info>
  **Rust** (via rustup) and **Cargo** are **pre-installed** on Devin's base image. Skip the install step if the default stable toolchain is sufficient. You only need the dependency fetch.
</Info>

```yaml theme={null}
initialize: |
  curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
  source ~/.cargo/env

maintenance: |
  cargo fetch

knowledge:
  - name: lint
    contents: |
      cargo clippy -- -D warnings

  - name: test
    contents: |
      cargo test

  - name: build
    contents: |
      cargo build --release
```

### Monorepos

<Tabs>
  <Tab title="Multi-language">
    Monorepo with a Node.js frontend and Python backend. Each subproject gets its own knowledge entries.

    ```yaml theme={null}
    initialize:
      - name: Install pnpm
        run: npm install -g pnpm
      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance:
      - name: Install frontend dependencies
        run: (cd packages/frontend && pnpm install)
      - name: Install backend dependencies
        run: (cd packages/backend && uv sync)
      - name: Build shared library
        run: (cd packages/shared && pnpm install && pnpm build)

    knowledge:
      - name: structure
        contents: |
          This is a monorepo with three packages:
          - `packages/frontend` — React app (TypeScript, pnpm)
          - `packages/backend` — Python API (FastAPI, uv)
          - `packages/shared` — Shared TypeScript utilities (must be built before frontend)
      - name: frontend
        contents: |
          Run `cd packages/frontend && pnpm dev` to start the dev server.
          Run `cd packages/frontend && pnpm lint` to lint.
          Run `cd packages/frontend && pnpm test` to test.
      - name: backend
        contents: |
          Run `cd packages/backend && uv run uvicorn app.main:app --reload` to start the API.
          Run `cd packages/backend && uv run ruff check .` to lint.
          Run `cd packages/backend && uv run pytest` to test.
    ```

    <Tip>
      Use subshells `(cd dir && command)` instead of `cd dir && command` so the working directory resets between steps.
    </Tip>
  </Tab>

  <Tab title="Multiple JDK versions">
    A Java monorepo where different services require different JDK versions. Install both JDKs at setup, then use `knowledge` entries to tell Devin which `JAVA_HOME` to use for each service.

    ```yaml theme={null}
    initialize:
      - name: Install JDK 17 (primary)
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install JDK 11 (legacy service)
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-11-jdk-headless

    maintenance:
      - name: Warm dependency caches
        run: |
          export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
          (cd services/api && ./gradlew dependencies --refresh-dependencies)

          export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
          (cd services/legacy && ./gradlew dependencies --refresh-dependencies)

    knowledge:
      - name: build_api
        contents: |
          Build the API service (JDK 17):
            JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
            cd services/api && ./gradlew clean build
      - name: build_legacy
        contents: |
          Build the legacy service (JDK 11):
            JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 \
            cd services/legacy && ./gradlew clean build
      - name: test_all
        contents: |
          Run tests for all services:
            JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
            (cd services/api && ./gradlew test)

            JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 \
            (cd services/legacy && ./gradlew test)
    ```
  </Tab>
</Tabs>

***

## Private package registries

Configure package managers to resolve dependencies from private registries. Set these in **Settings > Environment > Blueprints > Org-wide setup** (or per-repo if only one repo needs it).

<Warning>
  **Credential configuration belongs in `maintenance`, not `initialize`.** Steps that write secrets (registry passwords, auth tokens) into config files should use `maintenance` so credentials are freshly loaded each session. Secrets are removed before the snapshot is saved, so config files written during `initialize` won't have valid credentials when sessions start.
</Warning>

<Info>
  If your private registry uses a corporate CA, make sure the [CA certificate](#corporate-ca-certificate) is installed at the enterprise level first. The configuration below assumes HTTPS trust is already established.
</Info>

### Node.js registries

<Tabs>
  <Tab title="npm (scoped)">
    Configure npm to resolve scoped packages (e.g., `@myorg/*`) from a private registry, while public packages continue to come from the default npm registry.

    <Accordion title="Required secrets">
      * `GITHUB_PACKAGES_TOKEN` — Personal access token or GitHub App token with `read:packages` scope
    </Accordion>

    ```yaml theme={null}
    maintenance:
      - name: Configure npm scoped registry
        run: |
          npm config set @myorg:registry https://npm.pkg.github.com
          npm config set //npm.pkg.github.com/:_authToken $GITHUB_PACKAGES_TOKEN
    ```

    <Tip>
      Replace `@myorg` with your npm scope. Common private registry URLs:

      * **GitHub Packages:** `https://npm.pkg.github.com`
      * **Artifactory:** `https://artifactory.example.com/artifactory/api/npm/npm-virtual`
      * **Nexus:** `https://nexus.example.com/repository/npm-group`
      * **GitLab:** `https://gitlab.example.com/api/v4/packages/npm`
      * **AWS CodeArtifact:** `https://<domain>.d.codeartifact.<region>.amazonaws.com/npm/<repo>`
    </Tip>
  </Tab>

  <Tab title="npm (full mirror)">
    Route **all** npm packages through your private registry (not just scoped packages).

    <Accordion title="Required secrets">
      * `NPM_REGISTRY_URL` — Full URL of your npm registry (e.g., `https://artifactory.example.com/artifactory/api/npm/npm-virtual`)
      * `NPM_REGISTRY_HOST` — Hostname only, without protocol (e.g., `artifactory.example.com`)
      * `REGISTRY_TOKEN` — npm auth token for the registry
    </Accordion>

    ```yaml theme={null}
    maintenance:
      - name: Configure npm to use private registry
        run: |
          npm config set registry $NPM_REGISTRY_URL
          npm config set //${NPM_REGISTRY_HOST}/:_authToken $REGISTRY_TOKEN
          npm config set strict-ssl true
    ```
  </Tab>

  <Tab title="pnpm">
    Configure pnpm to resolve packages from a private registry.

    <Accordion title="Required secrets">
      * `NPM_REGISTRY_URL` — Full URL of your npm registry
      * `NPM_REGISTRY_HOST` — Hostname only, without protocol
      * `REGISTRY_TOKEN` — npm auth token for the registry
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install pnpm
        run: npm install -g pnpm

    maintenance:
      - name: Configure pnpm for private registry
        run: |
          pnpm config set registry $NPM_REGISTRY_URL
          pnpm config set //${NPM_REGISTRY_HOST}/:_authToken $REGISTRY_TOKEN
    ```
  </Tab>

  <Tab title="Yarn">
    Configure Yarn (Classic v1 or Berry v2+) to resolve packages from a private registry.

    <Accordion title="Required secrets">
      * `NPM_REGISTRY_URL` — Full URL of your npm/Yarn registry
      * `REGISTRY_TOKEN` — Auth token for the registry (Berry only)
    </Accordion>

    **Yarn Classic (v1):**

    ```yaml theme={null}
    initialize:
      - name: Install Yarn Classic
        run: npm install -g yarn

    maintenance:
      - name: Configure Yarn for private registry
        run: |
          yarn config set registry "$NPM_REGISTRY_URL"
          # For scoped packages:
          # yarn config set @myorg:registry "https://npm.pkg.github.com"
    ```

    **Yarn Berry (v2+):**

    ```yaml theme={null}
    maintenance:
      - name: Configure Yarn Berry for private registry
        run: |
          yarn config set npmRegistryServer "$NPM_REGISTRY_URL"
          yarn config set npmAuthToken "$REGISTRY_TOKEN"
          # For scoped packages:
          # yarn config set npmScopes.myorg.npmRegistryServer "https://npm.pkg.github.com"
          # yarn config set npmScopes.myorg.npmAuthToken "$GITHUB_PACKAGES_TOKEN"
    ```
  </Tab>
</Tabs>

### Python registries

<Tabs>
  <Tab title="pip / uv">
    Configure pip and uv to resolve packages from your private PyPI registry (e.g., Nexus, Artifactory).

    <Accordion title="Required secrets">
      * `PYPI_REGISTRY_URL` — Full URL of your PyPI index, including credentials if required (e.g., `https://user:token@nexus.example.com/repository/pypi-proxy/simple`)
    </Accordion>

    ```yaml theme={null}
    maintenance:
      - name: Configure pip/uv for private registry
        run: |
          mkdir -p ~/.config/pip
          cat > ~/.config/pip/pip.conf << EOF
          [global]
          index-url = $PYPI_REGISTRY_URL
          EOF

          echo "export UV_INDEX_URL=$PYPI_REGISTRY_URL" \
            | sudo tee /etc/profile.d/uv-registry.sh > /dev/null
    ```

    <Tip>
      Common PyPI registry URL patterns:

      * **Artifactory:** `https://artifactory.example.com/artifactory/api/pypi/pypi-virtual/simple`
      * **Nexus:** `https://nexus.example.com/repository/pypi-proxy/simple`
      * **AWS CodeArtifact:** `https://aws:TOKEN@domain-owner.d.codeartifact.region.amazonaws.com/pypi/repo/simple/`
      * **Azure Artifacts:** `https://pkgs.dev.azure.com/org/project/_packaging/feed/pypi/simple`
      * **GitLab:** `https://gitlab.example.com/api/v4/groups/<group-id>/-/packages/pypi/simple`
    </Tip>
  </Tab>

  <Tab title="Poetry">
    Configure Poetry to resolve packages from a private PyPI registry.

    <Accordion title="Required secrets">
      * `POETRY_REGISTRY_URL` — Full URL of your PyPI-compatible registry
      * `REGISTRY_USER` — Registry username
      * `REGISTRY_PASS` — Registry password or API token
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install Poetry
        run: curl -sSL https://install.python-poetry.org | python3 -

    maintenance:
      - name: Configure Poetry for private registry
        run: |
          poetry config repositories.private "$POETRY_REGISTRY_URL"
          poetry config http-basic.private "$REGISTRY_USER" "$REGISTRY_PASS"
    ```
  </Tab>
</Tabs>

### JVM registries

<Tabs>
  <Tab title="Maven">
    Install JDK and configure Maven to mirror all dependency resolution through your private registry (e.g., Artifactory, Nexus).

    <Info>
      JDK 17 is **pre-installed** on Devin's base image. Skip the install step if the default OpenJDK 17 is sufficient. You only need the Maven install and registry configuration.
    </Info>

    <Accordion title="Required secrets">
      * `MAVEN_REGISTRY_URL` — URL of your Maven registry (e.g., `https://artifactory.example.com/artifactory/maven-virtual`)
      * `REGISTRY_USER` — Registry username
      * `REGISTRY_PASS` — Registry password or API token
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install JDK 17
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Maven
        run: |
          MAVEN_VERSION=3.9.9
          curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
            | sudo tar -xz -C /opt
          sudo ln -sf /opt/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/local/bin/mvn

    maintenance:
      - name: Configure Maven for private registry
        run: |
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml << EOF
          <settings>
            <mirrors>
              <mirror>
                <id>private-registry</id>
                <mirrorOf>*</mirrorOf>
                <url>$MAVEN_REGISTRY_URL</url>
              </mirror>
            </mirrors>
            <servers>
              <server>
                <id>private-registry</id>
                <username>$REGISTRY_USER</username>
                <password>$REGISTRY_PASS</password>
              </server>
            </servers>
          </settings>
          EOF
    ```

    <Tip>
      Common registry URL patterns for Maven:

      * **Artifactory:** `https://artifactory.example.com/artifactory/maven-virtual`
      * **Nexus:** `https://nexus.example.com/repository/maven-public`
      * **Azure Artifacts:** `https://pkgs.dev.azure.com/org/project/_packaging/feed/maven/v1`
      * **GitHub Packages:** `https://maven.pkg.github.com`
      * **GitLab:** `https://gitlab.example.com/api/v4/groups/<group-id>/-/packages/maven`
      * **AWS CodeArtifact:** `https://<domain>.d.codeartifact.<region>.amazonaws.com/maven/<repo>`
    </Tip>
  </Tab>

  <Tab title="Gradle">
    Install JDK and configure Gradle to resolve all dependencies through your private registry.

    <Info>
      JDK 17 is **pre-installed** on Devin's base image. Skip the JDK install step if the default is sufficient.
    </Info>

    <Accordion title="Required secrets">
      * `GRADLE_REGISTRY_URL` — URL of your Gradle/Maven registry
      * `REGISTRY_USER` — Registry username
      * `REGISTRY_PASS` — Registry password or API token
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install JDK 17
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Gradle
        run: |
          GRADLE_VERSION=8.12
          curl -fsSL "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
            -o /tmp/gradle.zip
          sudo unzip -qo /tmp/gradle.zip -d /opt
          sudo ln -sf /opt/gradle-${GRADLE_VERSION}/bin/gradle /usr/local/bin/gradle
          rm /tmp/gradle.zip

    maintenance:
      - name: Configure Gradle for private registry
        run: |
          mkdir -p ~/.gradle
          cat > ~/.gradle/init.gradle << EOF
          allprojects {
              repositories {
                  maven {
                      url "$GRADLE_REGISTRY_URL"
                      credentials {
                          username = "$REGISTRY_USER"
                          password = "$REGISTRY_PASS"
                      }
                      allowInsecureProtocol = false
                  }
              }
          }
          EOF
    ```
  </Tab>
</Tabs>

### Other registries

<AccordionGroup>
  <Accordion title="Go module proxy">
    Install Go and configure it to resolve modules through a private module proxy (e.g., Athens, Artifactory, or a GOPROXY endpoint).

    <Accordion title="Required secrets">
      * `GO_PROXY_URL` — URL of your Go module proxy (e.g., `https://athens.corp.internal`)
      * `GIT_TOKEN` — Personal access token for private Git repos that host Go modules
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install Go
        run: |
          GO_VERSION=1.23.5
          ARCH=$(dpkg --print-architecture)
          curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${ARCH}.tar.gz" \
            | sudo tar -xz -C /usr/local
          echo 'export PATH="/usr/local/go/bin:$HOME/go/bin:$PATH"' \
            | sudo tee /etc/profile.d/golang.sh > /dev/null

      - name: Configure Go for private modules
        run: |
          cat << 'GOENV' | sudo tee /etc/profile.d/go-private.sh > /dev/null
          export GOPROXY="$GO_PROXY_URL,direct"
          export GONOSUMCHECK="corp.internal/*,github.com/myorg/*"
          export GOPRIVATE="corp.internal/*,github.com/myorg/*"
          GOENV

    maintenance:
      - name: Authenticate Go private modules
        run: |
          git config --global url."https://$GIT_TOKEN@github.com/myorg/".insteadOf "https://github.com/myorg/"
    ```

    <Tip>
      Common Go proxy URL patterns:

      * **Artifactory:** `https://artifactory.example.com/artifactory/go-virtual`
      * **Nexus:** `https://nexus.example.com/repository/go-proxy`
      * **Athens:** `https://athens.corp.internal`
    </Tip>
  </Accordion>

  <Accordion title=".NET / NuGet">
    Configure NuGet to resolve packages from a private feed.

    <Accordion title="Required secrets">
      * `NUGET_SOURCE_URL` — URL of your NuGet feed
      * `NUGET_API_KEY` — API key or PAT for the feed
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install .NET SDK
        run: |
          curl -fsSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0
          echo 'export PATH="$HOME/.dotnet:$PATH"' \
            | sudo tee /etc/profile.d/dotnet.sh > /dev/null

    maintenance:
      - name: Configure NuGet for private feed
        run: |
          dotnet nuget add source "$NUGET_SOURCE_URL" \
            --name private \
            --username any \
            --password "$NUGET_API_KEY" \
            --store-password-in-clear-text 2>/dev/null || \
          dotnet nuget update source private \
            --source "$NUGET_SOURCE_URL" \
            --username any \
            --password "$NUGET_API_KEY" \
            --store-password-in-clear-text
    ```
  </Accordion>

  <Accordion title="Docker">
    Configure Docker to pull from a private container registry.

    <Accordion title="Required secrets">
      * `DOCKER_MIRROR_URL` *(optional)* — URL of your Docker Hub mirror (e.g., `https://mirror.corp.internal`)
      * `DOCKER_REGISTRY_URL` — URL of your private container registry (e.g., `registry.corp.internal:5000`)
      * `DOCKER_REGISTRY_USER` — Registry username
      * `DOCKER_REGISTRY_PASS` — Registry password or API token
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Create Docker config directory
        run: sudo mkdir -p /etc/docker

    maintenance:
      - name: Configure Docker for private registry
        run: |
          # Configure registry mirror (optional — routes Docker Hub pulls through your registry)
          cat << EOF | sudo tee /etc/docker/daemon.json > /dev/null
          {
            "registry-mirrors": ["$DOCKER_MIRROR_URL"]
          }
          EOF
          sudo systemctl restart docker || true

          # Log in to the private container registry
          echo "$DOCKER_REGISTRY_PASS" | docker login "$DOCKER_REGISTRY_URL" \
            --username "$DOCKER_REGISTRY_USER" \
            --password-stdin
    ```

    <Tip>
      Common container registry URLs:

      * **Amazon ECR:** `<account-id>.dkr.ecr.<region>.amazonaws.com`
      * **Azure Container Registry:** `<name>.azurecr.io`
      * **Google Artifact Registry:** `<region>-docker.pkg.dev`
      * **GitHub Container Registry:** `ghcr.io`
      * **GitLab Container Registry:** `registry.gitlab.example.com`
      * **Nexus:** `https://nexus.example.com:8443`
      * **JFrog:** `<name>.jfrog.io`
    </Tip>
  </Accordion>

  <Accordion title="Rust / Cargo">
    Configure Cargo to resolve crates from a private registry.

    <Info>
      **Rust** (via rustup) and **Cargo** are **pre-installed** on Devin's base image. Skip the install step if the default stable toolchain is sufficient. You only need the registry configuration.
    </Info>

    <Accordion title="Required secrets">
      * `CARGO_REGISTRY_INDEX` — URL of the private registry index (e.g., `sparse+https://cargo.corp.internal/api/v1/crates/`)
      * `CARGO_REGISTRY_TOKEN` — Auth token for the private registry
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install Rust
        run: |
          curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
            | sh -s -- -y --default-toolchain stable
          echo 'source "$HOME/.cargo/env"' \
            | sudo tee /etc/profile.d/rust.sh > /dev/null

    maintenance:
      - name: Configure Cargo for private registry
        run: |
          mkdir -p ~/.cargo
          cat > ~/.cargo/config.toml << EOF
          [registries.private]
          index = "$CARGO_REGISTRY_INDEX"
          token = "$CARGO_REGISTRY_TOKEN"

          [source.crates-io]
          replace-with = "private"

          [source.private]
          registry = "$CARGO_REGISTRY_INDEX"
          EOF
    ```

    <Tip>
      If you only need to **add** a private registry without replacing crates.io, remove the `[source.crates-io]` and `[source.private]` sections and use `cargo install --registry private` or `[dependencies] my-crate = { version = "1.0", registry = "private" }` in `Cargo.toml`.
    </Tip>
  </Accordion>

  <Accordion title="Ruby / Bundler">
    Install Ruby and configure Bundler to resolve gems from a private gem server.

    <Accordion title="Required secrets">
      * `GEM_SERVER_URL` — URL of your private gem server (e.g., `https://artifactory.example.com/artifactory/api/gems/gems-virtual`)
      * `REGISTRY_USER` — Registry username
      * `REGISTRY_PASS` — Registry password or API token
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install Ruby
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ruby-full

    maintenance:
      - name: Configure Bundler for private gem server
        run: |
          bundle config set mirror.https://rubygems.org "$GEM_SERVER_URL"
          bundle config set "$GEM_SERVER_URL" "$REGISTRY_USER:$REGISTRY_PASS"
    ```

    <Tip>
      Common gem server URL patterns:

      * **Artifactory:** `https://artifactory.example.com/artifactory/api/gems/gems-virtual`
      * **Nexus:** `https://nexus.example.com/repository/rubygems-proxy`
      * **Gemfury:** `https://gem.fury.io/<org>`
    </Tip>
  </Accordion>

  <Accordion title="PHP / Composer">
    Install PHP and configure Composer to resolve packages from a private Packagist or Satis registry.

    <Accordion title="Required secrets">
      * `COMPOSER_REGISTRY_URL` — URL of your private Composer registry (e.g., `https://repo.packagist.com/<org>`)
      * `REGISTRY_USER` — Registry username
      * `REGISTRY_PASS` — Registry password or API token
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install PHP and Composer
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
            php-cli php-mbstring php-xml php-curl unzip

          # Install Composer
          curl -sS https://getcomposer.org/installer | php
          sudo mv composer.phar /usr/local/bin/composer

    maintenance:
      - name: Configure Composer for private registry
        run: |
          composer config --global repositories.private \
            composer "$COMPOSER_REGISTRY_URL"

          # Authenticate with the registry
          composer config --global http-basic.$(echo "$COMPOSER_REGISTRY_URL" \
            | sed 's|https\?://||;s|/.*||') "$REGISTRY_USER" "$REGISTRY_PASS"
    ```

    <Tip>
      Common Composer registry URL patterns:

      * **Artifactory:** `https://artifactory.example.com/artifactory/api/composer/packagist-virtual`
      * **Nexus:** `https://nexus.example.com/repository/packagist-proxy`
      * **Private Packagist:** `https://repo.packagist.com/<org>`
      * **Satis:** `https://satis.corp.internal`
    </Tip>
  </Accordion>

  <Accordion title="AWS CodeArtifact token refresh">
    AWS CodeArtifact tokens expire after 12 hours. Use `maintenance` to set up the token refresh command so the agent can re-run it when needed. This example configures npm, pip, and Maven to use CodeArtifact.

    <Info>
      **`awscli` is pre-installed** on Devin's base image. You only need the token refresh and registry configuration.
    </Info>

    <Accordion title="Required secrets">
      * `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` — IAM credentials with `codeartifact:GetAuthorizationToken` and `sts:GetServiceBearerToken` permissions
      * `CA_DOMAIN` — Your CodeArtifact domain name
      * `CA_DOMAIN_OWNER` — AWS account ID that owns the domain
      * `CA_REGION` — AWS region (e.g., `us-east-1`)
      * `CA_NPM_REPO`, `CA_PYPI_REPO`, `CA_MAVEN_REPO` — Repository names for each ecosystem
    </Accordion>

    ```yaml theme={null}
    maintenance:
      - name: Refresh CodeArtifact auth token
        run: |
          # Get a fresh token (valid for 12 hours)
          export CODEARTIFACT_AUTH_TOKEN=$(aws codeartifact get-authorization-token \
            --domain $CA_DOMAIN \
            --domain-owner $CA_DOMAIN_OWNER \
            --region $CA_REGION \
            --query authorizationToken \
            --output text)

          CA_ENDPOINT="https://${CA_DOMAIN}-${CA_DOMAIN_OWNER}.d.codeartifact.${CA_REGION}.amazonaws.com"

          # Configure npm
          npm config set registry "${CA_ENDPOINT}/npm/${CA_NPM_REPO}/"
          npm config set "//${CA_DOMAIN}-${CA_DOMAIN_OWNER}.d.codeartifact.${CA_REGION}.amazonaws.com/npm/${CA_NPM_REPO}/:_authToken" "$CODEARTIFACT_AUTH_TOKEN"

          # Configure pip
          mkdir -p ~/.config/pip
          cat > ~/.config/pip/pip.conf << EOF
          [global]
          index-url = https://aws:${CODEARTIFACT_AUTH_TOKEN}@${CA_DOMAIN}-${CA_DOMAIN_OWNER}.d.codeartifact.${CA_REGION}.amazonaws.com/pypi/${CA_PYPI_REPO}/simple/
          EOF

          # Configure Maven (optional)
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml << EOF
          <settings>
            <servers>
              <server>
                <id>codeartifact</id>
                <username>aws</username>
                <password>${CODEARTIFACT_AUTH_TOKEN}</password>
              </server>
            </servers>
            <mirrors>
              <mirror>
                <id>codeartifact</id>
                <mirrorOf>*</mirrorOf>
                <url>${CA_ENDPOINT}/maven/${CA_MAVEN_REPO}/</url>
              </mirror>
            </mirrors>
          </settings>
          EOF
    ```
  </Accordion>
</AccordionGroup>

***

## Enterprise infrastructure

Machine-level infrastructure that applies across all orgs and repos. Set these in **Settings > Devin's base environment** (for enterprise-wide) or **Settings > Environment > Blueprints > Org-wide setup** (for org-wide).

### Network and connectivity

<AccordionGroup>
  <Accordion title="Corporate CA certificate">
    Your organization uses a private certificate authority for internal services. Devin needs the root certificate to reach internal registries and tools over HTTPS.

    <Accordion title="Required secrets">
      * `CORP_ROOT_CA_B64` — Base64-encoded PEM certificate from your corporate CA. Generate with: `cat corp-root-ca.crt | base64 -w0`
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install corporate CA certificate
        run: |
          echo "$CORP_ROOT_CA_B64" | base64 -d \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          sudo update-ca-certificates
          echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
            | sudo tee /etc/profile.d/node-ca.sh > /dev/null
    ```
  </Accordion>

  <Accordion title="Multiple CA certificates">
    If your organization uses multiple CA certificates (e.g., separate CAs for different internal services).

    <Accordion title="Required secrets">
      * `CORP_ROOT_CA_B64` — Base64-encoded primary CA certificate
      * `CORP_INTERMEDIATE_CA_B64` — Base64-encoded intermediate CA certificate
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install corporate CA certificates
        run: |
          echo "$CORP_ROOT_CA_B64" | base64 -d \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          echo "$CORP_INTERMEDIATE_CA_B64" | base64 -d \
            | sudo tee /usr/local/share/ca-certificates/corp-intermediate-ca.crt > /dev/null
          sudo update-ca-certificates

          # Create a combined bundle for tools that need a single CA file
          cat /usr/local/share/ca-certificates/corp-*.crt \
            | sudo tee /usr/local/share/ca-certificates/corp-bundle.crt > /dev/null

          echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-bundle.crt' \
            | sudo tee /etc/profile.d/node-ca.sh > /dev/null
    ```
  </Accordion>

  <Accordion title="HTTP/HTTPS proxy">
    Route all network traffic through a corporate proxy.

    <Accordion title="Required secrets">
      * `CORP_HTTP_PROXY` — HTTP proxy URL (e.g., `http://proxy.corp.example.com:8080`)
      * `CORP_HTTPS_PROXY` — HTTPS proxy URL
      * `CORP_NO_PROXY` — Comma-separated list of hosts to bypass proxy (e.g., `localhost,127.0.0.1,.corp.example.com`)
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Configure system-wide proxy
        run: |
          cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
          export http_proxy="$CORP_HTTP_PROXY"
          export https_proxy="$CORP_HTTPS_PROXY"
          export no_proxy="$CORP_NO_PROXY"
          export HTTP_PROXY="$CORP_HTTP_PROXY"
          export HTTPS_PROXY="$CORP_HTTPS_PROXY"
          export NO_PROXY="$CORP_NO_PROXY"
          PROXY
          source /etc/profile.d/proxy.sh

    maintenance:
      - name: Configure git proxy
        run: |
          git config --global http.proxy "$CORP_HTTP_PROXY"
          git config --global https.proxy "$CORP_HTTPS_PROXY"

          # Configure npm proxy
          npm config set proxy "$CORP_HTTP_PROXY"
          npm config set https-proxy "$CORP_HTTPS_PROXY"
    ```
  </Accordion>

  <Accordion title="Authenticated proxy">
    If your corporate proxy requires username/password authentication.

    <Accordion title="Required secrets">
      * `PROXY_USER` — Proxy username
      * `PROXY_PASS` — Proxy password
      * `PROXY_HOST` — Proxy hostname and port (e.g., `proxy.corp.example.com:8080`)
      * `CORP_NO_PROXY` — Hosts to bypass proxy
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Configure authenticated proxy
        run: |
          PROXY_URL="http://${PROXY_USER}:${PROXY_PASS}@${PROXY_HOST}"

          cat << PROXY | sudo tee /etc/profile.d/proxy.sh > /dev/null
          export http_proxy="$PROXY_URL"
          export https_proxy="$PROXY_URL"
          export no_proxy="$CORP_NO_PROXY"
          export HTTP_PROXY="$PROXY_URL"
          export HTTPS_PROXY="$PROXY_URL"
          export NO_PROXY="$CORP_NO_PROXY"
          PROXY
          source /etc/profile.d/proxy.sh

    maintenance:
      - name: Configure git for authenticated proxy
        run: |
          PROXY_URL="http://${PROXY_USER}:${PROXY_PASS}@${PROXY_HOST}"
          git config --global http.proxy "$PROXY_URL"
          git config --global https.proxy "$PROXY_URL"
    ```
  </Accordion>

  <Accordion title="CA certificate + proxy (combined)">
    Combined setup for environments that need both a corporate CA and a proxy. This is common in enterprise environments where internal services use private certificates and all traffic must route through a proxy.

    <Accordion title="Required secrets">
      * `CORP_ROOT_CA_B64` — Base64-encoded corporate CA certificate
      * `CORP_HTTP_PROXY`, `CORP_HTTPS_PROXY` — Proxy URLs
      * `CORP_NO_PROXY` — Hosts to bypass proxy
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install corporate CA certificate
        run: |
          echo "$CORP_ROOT_CA_B64" | base64 -d \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          sudo update-ca-certificates
          echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
            | sudo tee /etc/profile.d/node-ca.sh > /dev/null

      - name: Configure system-wide proxy
        run: |
          cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
          export http_proxy="$CORP_HTTP_PROXY"
          export https_proxy="$CORP_HTTPS_PROXY"
          export no_proxy="$CORP_NO_PROXY"
          export HTTP_PROXY="$CORP_HTTP_PROXY"
          export HTTPS_PROXY="$CORP_HTTPS_PROXY"
          export NO_PROXY="$CORP_NO_PROXY"
          PROXY
          source /etc/profile.d/proxy.sh

    maintenance:
      - name: Configure git proxy
        run: |
          git config --global http.proxy "$CORP_HTTP_PROXY"
          git config --global https.proxy "$CORP_HTTPS_PROXY"
    ```
  </Accordion>

  <Accordion title="VPN connection">
    Your private registries, Git servers, or other internal services are only reachable over VPN. This must run **before** other modules that need network access to internal resources.

    <Accordion title="Required secrets">
      **OpenVPN:**

      * `VPN_CONFIG_B64` — Base64-encoded OpenVPN config file (`.ovpn`). Generate with: `cat corp.ovpn | base64 -w0`
      * `VPN_AUTH_USER` *(optional)* — VPN username, if your VPN requires username/password auth
      * `VPN_AUTH_PASS` *(optional)* — VPN password

      **WireGuard:**

      * `WG_CONFIG_B64` — Base64-encoded WireGuard config file. Generate with: `cat wg0.conf | base64 -w0`
    </Accordion>

    **OpenVPN:**

    ```yaml theme={null}
    initialize:
      - name: Install and configure OpenVPN
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openvpn

          # Write the VPN config
          sudo mkdir -p /etc/openvpn/client
          echo "$VPN_CONFIG_B64" | base64 -d \
            | sudo tee /etc/openvpn/client/corp.conf > /dev/null

          # If VPN requires username/password auth
          if [ -n "${VPN_AUTH_USER:-}" ] && [ -n "${VPN_AUTH_PASS:-}" ]; then
            printf '%s\n%s\n' "$VPN_AUTH_USER" "$VPN_AUTH_PASS" \
              | sudo tee /etc/openvpn/client/auth.txt > /dev/null
            sudo chmod 600 /etc/openvpn/client/auth.txt
            echo "auth-user-pass /etc/openvpn/client/auth.txt" \
              | sudo tee -a /etc/openvpn/client/corp.conf > /dev/null
          fi

          # Start the VPN tunnel
          sudo systemctl daemon-reload
          sudo systemctl enable --now openvpn-client@corp

          # Wait for the tunnel to come up
          for i in $(seq 1 30); do
            if ip link show tun0 >/dev/null 2>&1; then break; fi
            sleep 1
          done
    ```

    **WireGuard:**

    ```yaml theme={null}
    initialize:
      - name: Install and configure WireGuard
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq wireguard-tools

          # Write the WireGuard config
          echo "$WG_CONFIG_B64" | base64 -d \
            | sudo tee /etc/wireguard/wg0.conf > /dev/null
          sudo chmod 600 /etc/wireguard/wg0.conf

          # Start the tunnel
          sudo systemctl enable --now wg-quick@wg0
    ```

    <Tip>
      For more details on VPN setup, see [VPN Configuration](/onboard-devin/vpn).
    </Tip>
  </Accordion>

  <Accordion title="Custom DNS resolution">
    Your internal services use private DNS names that aren't resolvable by public DNS.

    ```yaml theme={null}
    initialize:
      - name: Configure custom DNS resolution
        run: |
          # Add internal hostnames
          cat << 'HOSTS' | sudo tee -a /etc/hosts > /dev/null
          10.0.1.50  nexus.corp.internal
          10.0.1.51  git.corp.internal
          10.0.1.52  artifactory.corp.internal
          HOSTS

          # Optionally configure custom nameservers
          sudo mkdir -p /etc/systemd/resolved.conf.d
          cat << 'DNS' | sudo tee /etc/systemd/resolved.conf.d/corp.conf > /dev/null
          [Resolve]
          DNS=10.0.0.53 10.0.0.54
          Domains=corp.internal
          DNS
          sudo systemctl restart systemd-resolved || true
    ```
  </Accordion>
</AccordionGroup>

### Identity and security

<AccordionGroup>
  <Accordion title="GPG commit signing">
    Your organization requires all Git commits to be signed, and you want GitHub to mark Devin's commits as **Verified**.

    <Accordion title="Required secrets">
      * `GPG_PRIVATE_KEY_B64` — Base64-encoded GPG private key. Generate with: `gpg --export-secret-keys <key-id> | base64 -w0`
      * `GIT_USER_NAME` — Git author name (e.g., `Devin AI`)
      * `GIT_USER_EMAIL` — Git author email. **Must match a UID on the GPG key**, otherwise GitHub will not verify the signature.
    </Accordion>

    <Note>
      Also upload the matching **public** key to the GitHub account whose credentials Devin uses to push (under [GitHub Settings > SSH and GPG keys](https://github.com/settings/keys)). GitHub only marks commits as Verified when the signing public key is registered on the account that authored the commit.
    </Note>

    ```yaml theme={null}
    initialize:
      - name: Prepare GPG and git signing config
        run: |
          # Allow GPG to work without a TTY
          echo 'export GPG_TTY=$(tty)' | sudo tee -a /etc/profile.d/gpg.sh > /dev/null

    maintenance:
      - name: Import GPG key and configure git signing
        run: |
          echo "$GPG_PRIVATE_KEY_B64" | base64 -d | gpg --batch --import 2>/dev/null

          KEY_ID=$(gpg --list-secret-keys --keyid-format long 2>/dev/null \
            | grep sec | head -1 | awk '{print $2}' | cut -d'/' -f2)

          git config --global user.signingkey "$KEY_ID"
          git config --global commit.gpgsign true
          git config --global tag.gpgsign true
          git config --global gpg.program gpg
    ```
  </Accordion>

  <Accordion title="Git identity and SSH keys">
    Configure Devin's Git identity and SSH keys for accessing private Git servers.

    <Accordion title="Required secrets">
      * `GIT_USER_NAME` — Git author name
      * `GIT_USER_EMAIL` — Git author email
      * `SSH_PRIVATE_KEY_B64` — Base64-encoded SSH private key. Generate with: `cat ~/.ssh/id_ed25519 | base64 -w0`
      * `SSH_KNOWN_HOSTS_B64` — Base64-encoded known hosts entries. Generate with: `ssh-keyscan git.corp.internal | base64 -w0`
      * `SSH_CONFIG_B64` *(optional)* — Base64-encoded SSH config file
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Configure git identity
        run: |
          git config --global user.name "$GIT_USER_NAME"
          git config --global user.email "$GIT_USER_EMAIL"

          # Prepare SSH directory
          mkdir -p ~/.ssh && chmod 700 ~/.ssh

    maintenance:
      - name: Install SSH keys
        run: |
          # Install SSH private key (in maintenance so it's freshly loaded each session)
          echo "$SSH_PRIVATE_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519

          # Add known hosts for your Git server
          echo "$SSH_KNOWN_HOSTS_B64" | base64 -d >> ~/.ssh/known_hosts

          # Optionally install a custom SSH config
          if [ -n "${SSH_CONFIG_B64:-}" ]; then
            echo "$SSH_CONFIG_B64" | base64 -d > ~/.ssh/config
            chmod 600 ~/.ssh/config
          fi
    ```

    <Tip>
      Generate the known hosts entry for your Git server with `ssh-keyscan git.corp.internal | base64 -w0`.
    </Tip>
  </Accordion>
</AccordionGroup>

### System configuration

<AccordionGroup>
  <Accordion title="System packages">
    Install system-level packages that aren't in the default Devin image (e.g., native libraries for image processing or PDF generation).

    ```yaml theme={null}
    initialize:
      - name: Install system packages
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
            libpq-dev \
            libmagickwand-dev \
            poppler-utils \
            ffmpeg
    ```
  </Accordion>

  <Accordion title="Custom environment variables">
    Set persistent environment variables that should be available in every session.

    The recommended approach is to write `KEY=VALUE` lines to the `$ENVRC` file. Variables written to `$ENVRC` are automatically exported for all subsequent steps and the Devin session (similar to GitHub Actions' `$GITHUB_ENV`).

    ```yaml theme={null}
    initialize:
      - name: Set custom environment variables
        run: |
          echo "CORPORATE_ENV=production" >> $ENVRC
          echo "DEFAULT_REGION=us-east-1" >> $ENVRC
          echo "MAX_RETRIES=3" >> $ENVRC
    ```

    <Tip>
      You can also write env vars to `/etc/profile.d/` scripts for system-wide availability:

      ```bash theme={null}
      cat << 'ENVVARS' | sudo tee /etc/profile.d/custom-env.sh > /dev/null
      export CORPORATE_ENV=production
      export DEFAULT_REGION=us-east-1
      ENVVARS
      ```

      Both approaches work. `$ENVRC` is simpler and recommended for most cases.
    </Tip>
  </Accordion>

  <Accordion title="Locale and timezone">
    Default base images may have broken locale settings. Configure locale and timezone to prevent warnings from build tools, Java, Python, and Git.

    ```yaml theme={null}
    initialize:
      - name: Configure locale and timezone
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq locales

          # Generate and set the locale
          sudo sed -i 's/^# *en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen
          sudo locale-gen
          sudo update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8

          cat << 'LOCALE' | sudo tee /etc/profile.d/locale.sh > /dev/null
          export LANG="en_US.UTF-8"
          export LC_ALL="en_US.UTF-8"
          LOCALE

          # Set timezone
          sudo timedatectl set-timezone UTC 2>/dev/null || \
            sudo ln -sfn /usr/share/zoneinfo/UTC /etc/localtime
    ```
  </Accordion>

  <Accordion title="Resource limits (ulimits)">
    Java, Gradle, and Node.js builds frequently hit the default 1024 open-file limit. Raise it to prevent build failures.

    ```yaml theme={null}
    initialize:
      - name: Raise resource limits
        run: |
          cat << 'LIMITS' | sudo tee /etc/security/limits.d/99-devin.conf > /dev/null
          *    soft    nofile    65536
          *    hard    nofile    65536
          *    soft    nproc     65536
          *    hard    nproc     65536
          LIMITS

          # Also set the kernel max
          echo "fs.file-max = 65536" | sudo tee /etc/sysctl.d/99-devin-filemax.conf > /dev/null
          sudo sysctl -p /etc/sysctl.d/99-devin-filemax.conf 2>/dev/null || true
    ```
  </Accordion>

  <Accordion title="APT mirror replacement">
    In air-gapped or restricted environments, replace the default Ubuntu APT sources with an internal mirror.

    <Accordion title="Required secrets">
      * `APT_MIRROR_URL` — URL of your internal APT mirror (e.g., `https://artifactory.example.com/artifactory/ubuntu-remote`)
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Replace APT sources with internal mirror
        run: |
          # Backup original sources
          sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak

          # Replace all Ubuntu mirrors with your internal mirror
          sudo sed -i "s|http://archive.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list
          sudo sed -i "s|http://security.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list

          sudo apt-get update -qq
    ```

    <Tip>
      Common APT mirror URL patterns:

      * **Artifactory:** `https://artifactory.example.com/artifactory/ubuntu-remote`
      * **Nexus:** `https://nexus.example.com/repository/ubuntu-proxy`
    </Tip>
  </Accordion>
</AccordionGroup>

***

## Advanced patterns

<AccordionGroup>
  <Accordion title="Environment variables with direnv">
    Devin's base environment includes [direnv](https://direnv.net/). Use `initialize` to create `.envrc` files. Direnv loads them automatically.

    ```yaml theme={null}
    initialize: |
      cat <<'EOF' > .envrc
      export DATABASE_URL=postgresql://localhost:5432/myapp_dev
      export REDIS_URL=redis://localhost:6379
      export APP_ENV=development
      EOF

    maintenance: |
      direnv allow .
    ```

    <Info>
      direnv is pre-hooked into Devin's shell, so `.envrc` variables load automatically. No manual sourcing needed.
    </Info>

    <Tip>
      For **sensitive** environment variables (API keys, tokens, database passwords), use [repo secrets](/product-guides/secrets) instead of `.envrc` files. Repo secrets are stored securely and injected at session time. They never appear in your blueprint or snapshot.
    </Tip>
  </Accordion>

  <Accordion title="Per-repository Node version switching">
    Use nvm (pre-installed) to switch Node.js versions per repository via `.nvmrc`.

    ```yaml theme={null}
    initialize: |
      nvm install 18
      nvm install 20
      nvm install 22

    maintenance: |
      nvm use
    ```

    <Info>
      `nvm use` reads `.nvmrc` from the repo root. Make sure your repository has one (e.g., containing `20`).
    </Info>
  </Accordion>

  <Accordion title="Browser authentication (Playwright)">
    Devin provides a Chrome browser with a CDP endpoint at `localhost:29229` **during sessions**. Use Playwright scripts to automate browser-based login.

    <Warning>
      The browser is only available during sessions, not snapshot builds. Install Playwright in `initialize` and keep login scripts in your repo.
    </Warning>

    ```yaml theme={null}
    initialize: |
      pip install playwright
      playwright install chromium

    maintenance: |
      npm install

    knowledge:
      - name: browser-auth
        contents: |
          This project requires browser authentication.
          Run the login script before interacting with the app:
          python scripts/login.py

          Devin's Chrome browser is accessible via CDP at:
          http://localhost:29229
    ```

    Example login script (`scripts/login.py`):

    ```python theme={null}
    from playwright.sync_api import sync_playwright
    import os

    with sync_playwright() as p:
        browser = p.chromium.connect_over_cdp("http://localhost:29229")
        context = browser.contexts[0]
        page = context.pages[0] if context.pages else context.new_page()

        page.goto("https://internal-tool.example.com/login")
        page.fill("#username", os.environ["TOOL_USERNAME"])
        page.fill("#password", os.environ["TOOL_PASSWORD"])
        page.click('button[type="submit"]')
        page.wait_for_url("**/dashboard")
    ```

    <Warning>
      Store login credentials as secrets, not in source code. For long-lived authentication, commit login scripts to `.agents/skills/` so Devin can re-authenticate automatically.
    </Warning>
  </Accordion>

  <Accordion title="Custom system tools and PATH">
    Install system packages, custom binaries, and configure PATH in `initialize`.

    ```yaml theme={null}
    initialize:
      - name: Install system packages
        run: |
          apt-get update
          apt-get install -y \
            jq \
            ripgrep \
            fd-find \
            protobuf-compiler \
            libssl-dev

      - name: Install custom CLI tool
        run: |
          curl -L https://github.com/example/tool/releases/download/v1.0/tool-linux-amd64 \
            -o /usr/local/bin/mytool
          chmod +x /usr/local/bin/mytool

      - name: Add custom bin directory to PATH
        run: |
          mkdir -p ~/bin
          echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc

    knowledge:
      - name: tools
        contents: |
          Custom tools available:
          - mytool: installed at /usr/local/bin/mytool
          - Additional binaries can be placed in ~/bin
    ```
  </Accordion>

  <Accordion title="GitHub Actions for tool setup">
    Devin supports running Node.js-based GitHub Actions directly in blueprints. This is useful for installing specific tool versions via the same actions your CI uses.

    ```yaml theme={null}
    initialize:
      - name: "Install Node.js 20"
        uses: github.com/actions/setup-node@v4
        with:
          node-version: "20"

      - name: "Install Python 3.12"
        uses: github.com/actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: "Install Go 1.22"
        uses: github.com/actions/setup-go@v5
        with:
          go-version: "1.22"

      - name: "Install Java 21"
        uses: github.com/actions/setup-java@v4
        with:
          java-version: "21"
          distribution: "temurin"

      - name: "Install Gradle"
        uses: github.com/gradle/actions/setup-gradle@v4

      - name: "Install Ruby 3.3"
        uses: github.com/ruby/setup-ruby@v1
        with:
          ruby-version: "3.3"

      - name: "Install additional tools"
        run: |
          pip install uv
          npm install -g pnpm turbo
          go install golang.org/x/tools/gopls@latest

    maintenance: |
      echo "All runtimes are available:"
      node --version
      python --version
      go version
      java --version
    ```

    <Info>
      Actions like `setup-node` and `setup-python` modify PATH and environment variables. Binaries installed by one action are available in all subsequent steps and in `maintenance`. Only **Node.js-based** GitHub Actions are supported. Composite and Docker-based actions are not.
    </Info>

    <Tip>
      You don't need GitHub Actions for basic tool setup. Direct shell commands (`nvm install 20`, `curl ... | sh`, `apt-get install`) work just as well and are often simpler. GitHub Actions are most useful when you want to match your CI setup exactly or need the convenience of actions like `setup-java` which handle multiple distributions.
    </Tip>
  </Accordion>

  <Accordion title="Local HTTPS reverse proxy for multiple apps">
    Run several services behind real-looking hostnames over HTTPS, such as `app.example.com`, `api.example.com`, and `admin.example.com`. Install a single reverse proxy in `initialize` and route each hostname to a different local upstream port.

    [Caddy](https://caddyserver.com/) handles routing and local TLS in one tool. A Caddyfile maps each hostname to an upstream, and `tls internal` auto-issues a trusted certificate per hostname from Caddy's built-in CA. `caddy trust` installs that CA root into the system trust store, and adding the same root to the NSS database lets the browser accept it.

    Upload your Caddyfile via the **File attachments** section of the blueprint editor; it is then available as `$FILE_CADDYFILE`.

    ```caddyfile Caddyfile theme={null}
    app.example.com {
      tls internal
      reverse_proxy 127.0.0.1:3000
    }

    api.example.com {
      tls internal
      reverse_proxy 127.0.0.1:3001
    }

    admin.example.com {
      tls internal
      reverse_proxy 127.0.0.1:3002
    }
    ```

    ```yaml theme={null}
    initialize:
      - name: Install Caddy and NSS tools
        run: |
          sudo apt-get install -y debian-keyring debian-archive-keyring \
            apt-transport-https curl libnss3-tools
          curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
            | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
          curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
            | sudo tee /etc/apt/sources.list.d/caddy-stable.list
          sudo apt-get update -qq
          sudo apt-get install -y caddy

      - name: Install Caddyfile, hosts entries, and trust internal CA
        run: |
          sudo install -m 0644 "$FILE_CADDYFILE" /etc/caddy/Caddyfile

          for host in app.example.com api.example.com admin.example.com; do
            grep -q " $host$" /etc/hosts \
              || echo "127.0.0.1 $host" | sudo tee -a /etc/hosts > /dev/null
          done

          sudo systemctl enable --now caddy
          sudo caddy trust

          mkdir -p ~/.pki/nssdb
          [ -f ~/.pki/nssdb/cert9.db ] \
            || certutil -d sql:$HOME/.pki/nssdb -N --empty-password

          CADDY_ROOT=/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt
          for _ in $(seq 1 30); do
            sudo test -f "$CADDY_ROOT" && break
            sleep 1
          done

          if sudo test -f "$CADDY_ROOT" \
             && ! certutil -d sql:$HOME/.pki/nssdb -L | grep -q "Caddy Local Authority"; then
            sudo install -o "$USER" -m 0644 "$CADDY_ROOT" /tmp/caddy-root.crt
            certutil -d sql:$HOME/.pki/nssdb -A -t "C,," \
              -n "Caddy Local Authority" -i /tmp/caddy-root.crt
            rm -f /tmp/caddy-root.crt
          fi
    ```

    <Info>
      The `/etc/hosts` loop is what makes `app.example.com` resolve to `127.0.0.1` inside the session. Add an entry for every hostname you put in the Caddyfile.
    </Info>

    <Tip>
      To add a service, append a three-line block to the Caddyfile and one entry to the `/etc/hosts` loop, then visit its hostname over HTTPS. Caddy mints a certificate on first request, so no per-app cert generation is needed.
    </Tip>
  </Accordion>
</AccordionGroup>

***

## Full-stack examples

These examples show how enterprise and org-level configurations combine. In practice, you'd split these across scopes. They're shown together here for reference.

<AccordionGroup>
  <Accordion title="Full enterprise stack (Artifactory)">
    A complete enterprise environment: corporate CA certificate, proxy, Java (Maven), Python (pip/uv), Node.js (npm), and Docker, all pointed at a single Artifactory instance.

    <Accordion title="Required secrets">
      **Network & trust (account-wide):**

      * `CORP_ROOT_CA_B64` — Base64-encoded corporate CA certificate
      * `CORP_HTTP_PROXY` — HTTP proxy URL
      * `CORP_HTTPS_PROXY` — HTTPS proxy URL
      * `CORP_NO_PROXY` — Hosts to bypass proxy

      **Registry credentials (org-wide):**

      * `ARTIFACTORY_USER` — Artifactory username
      * `ARTIFACTORY_TOKEN` — Artifactory API token or password
      * `ARTIFACTORY_MAVEN_URL` — Maven repository URL (e.g., `https://artifactory.example.com/artifactory/maven-virtual`)
      * `ARTIFACTORY_PYPI_URL` — PyPI repository URL (e.g., `https://user:token@artifactory.example.com/artifactory/api/pypi/pypi-virtual/simple`)
      * `ARTIFACTORY_NPM_URL` — npm repository URL (e.g., `https://artifactory.example.com/artifactory/api/npm/npm-virtual`)
      * `ARTIFACTORY_DOCKER_URL` — Docker registry URL (e.g., `artifactory.example.com`)
    </Accordion>

    This would typically be split across three scopes:

    * **Account-wide (`initialize`):** Certificate and proxy
    * **Org-wide (`initialize`):** Language runtime installs
    * **Org-wide (`maintenance`):** Registry credentials (refreshed during builds, surfaced to agent at session start)

    Shown here combined for reference:

    ```yaml theme={null}
    initialize:
      # ── Account-wide: network and trust ──────────────────────────────────────

      - name: Install corporate CA certificate
        run: |
          echo "$CORP_ROOT_CA_B64" | base64 -d \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          sudo update-ca-certificates
          echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
            | sudo tee /etc/profile.d/node-ca.sh > /dev/null

      - name: Configure system-wide proxy
        run: |
          cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
          export http_proxy="$CORP_HTTP_PROXY"
          export https_proxy="$CORP_HTTPS_PROXY"
          export no_proxy="$CORP_NO_PROXY"
          export HTTP_PROXY="$CORP_HTTP_PROXY"
          export HTTPS_PROXY="$CORP_HTTPS_PROXY"
          export NO_PROXY="$CORP_NO_PROXY"
          PROXY
          source /etc/profile.d/proxy.sh

      # ── Org-wide: language runtimes ──────────────────────────────────────────

      - name: Install JDK 17 + Maven
        run: |
          sudo apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

          MAVEN_VERSION=3.9.9
          curl -fsSL "https://archive.apache.org/dist/maven/maven-3/${MAVEN_VERSION}/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz" \
            | sudo tar -xz -C /opt
          sudo ln -sf /opt/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/local/bin/mvn

      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance:
      # ── Account-wide: git proxy (refreshed each session) ───────────────────

      - name: Configure git proxy
        run: |
          git config --global http.proxy "$CORP_HTTP_PROXY"
          git config --global https.proxy "$CORP_HTTPS_PROXY"

      # ── Org-wide: registry credentials (refreshed each session) ──────────────

      - name: Configure Maven → Artifactory
        run: |
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml << EOF
          <settings>
            <mirrors>
              <mirror>
                <id>artifactory</id>
                <mirrorOf>*</mirrorOf>
                <url>$ARTIFACTORY_MAVEN_URL</url>
              </mirror>
            </mirrors>
            <servers>
              <server>
                <id>artifactory</id>
                <username>$ARTIFACTORY_USER</username>
                <password>$ARTIFACTORY_TOKEN</password>
              </server>
            </servers>
          </settings>
          EOF

      - name: Configure pip/uv → Artifactory PyPI
        run: |
          mkdir -p ~/.config/pip
          cat > ~/.config/pip/pip.conf << EOF
          [global]
          index-url = $ARTIFACTORY_PYPI_URL
          trusted-host = $(echo "$ARTIFACTORY_PYPI_URL" | sed 's|https\?://||;s|/.*||')
          EOF

          echo "export UV_INDEX_URL=$ARTIFACTORY_PYPI_URL" \
            | sudo tee /etc/profile.d/uv-registry.sh > /dev/null

      - name: Configure npm → Artifactory
        run: |
          npm config set registry "$ARTIFACTORY_NPM_URL"
          REGISTRY_HOST=$(echo "$ARTIFACTORY_NPM_URL" | sed 's|https\?://||;s|/.*||')
          npm config set "//${REGISTRY_HOST}/:_authToken" "$ARTIFACTORY_TOKEN"

      - name: Configure Docker → Artifactory
        run: |
          echo "$ARTIFACTORY_TOKEN" | docker login "$ARTIFACTORY_DOCKER_URL" \
            --username "$ARTIFACTORY_USER" \
            --password-stdin
    ```

    <Info>
      In this example, all registries point at the same Artifactory instance but use different URL paths. Each package ecosystem has its own endpoint format. Maven, PyPI, npm, and Docker URLs are all different even for the same registry.
    </Info>
  </Accordion>

  <Accordion title="Multi-language with different registries">
    When different languages use different private registries (e.g., Maven from Nexus, npm from GitHub Packages, Python from Artifactory).

    <Accordion title="Required secrets">
      * `NEXUS_MAVEN_URL` — Nexus Maven repository URL
      * `NEXUS_USER` — Nexus username
      * `NEXUS_PASS` — Nexus password
      * `GITHUB_PACKAGES_TOKEN` — GitHub personal access token with `read:packages` scope
      * `ARTIFACTORY_USER` — Artifactory username
      * `ARTIFACTORY_TOKEN` — Artifactory API token
      * `GIT_TOKEN` — Personal access token for Go private modules
    </Accordion>

    ```yaml theme={null}
    maintenance:
      # Maven → Nexus
      - name: Configure Maven → Nexus
        run: |
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml << EOF
          <settings>
            <mirrors>
              <mirror>
                <id>nexus</id>
                <mirrorOf>*</mirrorOf>
                <url>$NEXUS_MAVEN_URL</url>
              </mirror>
            </mirrors>
            <servers>
              <server>
                <id>nexus</id>
                <username>$NEXUS_USER</username>
                <password>$NEXUS_PASS</password>
              </server>
            </servers>
          </settings>
          EOF

      # npm → GitHub Packages (scoped)
      - name: Configure npm → GitHub Packages
        run: |
          npm config set @myorg:registry https://npm.pkg.github.com
          npm config set //npm.pkg.github.com/:_authToken $GITHUB_PACKAGES_TOKEN

      # Python → Artifactory
      - name: Configure pip → Artifactory
        run: |
          mkdir -p ~/.config/pip
          cat > ~/.config/pip/pip.conf << EOF
          [global]
          index-url = https://$ARTIFACTORY_USER:$ARTIFACTORY_TOKEN@artifactory.example.com/artifactory/api/pypi/pypi-virtual/simple
          EOF

      # Go → private modules via git
      - name: Configure Go private modules
        run: |
          git config --global url."https://$GIT_TOKEN@github.com/myorg/".insteadOf "https://github.com/myorg/"
    ```
  </Accordion>

  <Accordion title="Air-gapped environment with private mirrors">
    In a fully air-gapped environment, Devin cannot reach any public URLs. All tools, runtimes, and packages must come from internal mirrors.

    <Accordion title="Required secrets">
      **Certificates:**

      * `CORP_ROOT_CA_B64` — Base64-encoded corporate CA certificate

      **Mirror access:**

      * `APT_MIRROR_URL` — Internal Ubuntu APT mirror URL
      * `MIRROR_USER` — Mirror authentication username
      * `MIRROR_PASS` — Mirror authentication password
      * `JDK_TARBALL_URL` — URL to download JDK tarball from internal mirror
      * `NODE_TARBALL_URL` — URL to download Node.js tarball from internal mirror

      **Package registries:**

      * `INTERNAL_MAVEN_URL` — Internal Maven registry URL
      * `INTERNAL_NPM_URL` — Internal npm registry URL
      * `INTERNAL_PYPI_URL` — Internal PyPI registry URL
    </Accordion>

    ```yaml theme={null}
    initialize:
      - name: Install corporate CA certificate
        run: |
          echo "$CORP_ROOT_CA_B64" | base64 -d \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          sudo update-ca-certificates

      - name: Replace apt sources with internal mirror
        run: |
          sudo sed -i "s|http://archive.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list
          sudo sed -i "s|http://security.ubuntu.com/ubuntu|$APT_MIRROR_URL|g" /etc/apt/sources.list
          sudo apt-get update -qq

      - name: Install JDK from internal mirror
        run: |
          # Download JDK tarball from internal artifact store
          curl -fsSL -u "$MIRROR_USER:$MIRROR_PASS" "$JDK_TARBALL_URL" \
            | sudo tar -xz -C /usr/local
          sudo ln -sf /usr/local/jdk-17.*/bin/java /usr/local/bin/java
          sudo ln -sf /usr/local/jdk-17.*/bin/javac /usr/local/bin/javac
          echo "export JAVA_HOME=$(ls -d /usr/local/jdk-17.*)" \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Node.js from internal mirror
        run: |
          curl -fsSL -u "$MIRROR_USER:$MIRROR_PASS" "$NODE_TARBALL_URL" \
            | sudo tar -xz -C /usr/local --strip-components=1

    maintenance:
      - name: Configure all package managers for internal registry
        run: |
          # Maven
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml << EOF
          <settings>
            <mirrors>
              <mirror>
                <id>internal</id>
                <mirrorOf>*</mirrorOf>
                <url>$INTERNAL_MAVEN_URL</url>
              </mirror>
            </mirrors>
            <servers>
              <server>
                <id>internal</id>
                <username>$MIRROR_USER</username>
                <password>$MIRROR_PASS</password>
              </server>
            </servers>
          </settings>
          EOF

          # npm
          npm config set registry "$INTERNAL_NPM_URL"

          # pip
          mkdir -p ~/.config/pip
          cat > ~/.config/pip/pip.conf << EOF
          [global]
          index-url = $INTERNAL_PYPI_URL
          EOF
    ```

    <Warning>
      In air-gapped environments, all tools Devin needs (language runtimes, CLI tools, etc.) must be available on your internal mirrors. Public registries and download sites are unreachable.
    </Warning>
  </Accordion>

  <Accordion title="VPN + certificates + proxy + languages">
    A comprehensive enterprise setup combining VPN connectivity with certificates, proxy, and multi-language support. This is the recommended order of operations.

    <Accordion title="Required secrets">
      **VPN:**

      * `VPN_CONFIG_B64` — Base64-encoded OpenVPN config file

      **Network & trust:**

      * `CORP_ROOT_CA_B64` — Base64-encoded corporate CA certificate
      * `CORP_HTTP_PROXY` — HTTP proxy URL
      * `CORP_HTTPS_PROXY` — HTTPS proxy URL
      * `CORP_NO_PROXY` — Hosts to bypass proxy

      **Registry credentials:**

      * `MAVEN_REGISTRY_URL` — Maven registry URL
      * `NPM_REGISTRY_URL` — npm registry URL
      * `PYPI_REGISTRY_HOST` — PyPI registry hostname
      * `REGISTRY_USER` — Registry username (for Maven and pip)
      * `REGISTRY_PASS` — Registry password (for Maven and pip)
      * `REGISTRY_TOKEN` — npm auth token
    </Accordion>

    ```yaml theme={null}
    initialize:
      # 1. VPN — must come first so internal resources are reachable
      - name: Establish VPN connection
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get update -qq
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openvpn
          sudo mkdir -p /etc/openvpn/client
          echo "$VPN_CONFIG_B64" | base64 -d \
            | sudo tee /etc/openvpn/client/corp.conf > /dev/null
          sudo systemctl daemon-reload
          sudo systemctl enable --now openvpn-client@corp
          for i in $(seq 1 30); do
            if ip link show tun0 >/dev/null 2>&1; then break; fi
            sleep 1
          done

      # 2. DNS — resolve internal hostnames
      - name: Configure DNS
        run: |
          sudo mkdir -p /etc/systemd/resolved.conf.d
          cat << 'DNS' | sudo tee /etc/systemd/resolved.conf.d/corp.conf > /dev/null
          [Resolve]
          DNS=10.0.0.53
          Domains=corp.internal
          DNS
          sudo systemctl restart systemd-resolved || true

      # 3. Certificates — trust internal CAs
      - name: Install CA certificate
        run: |
          echo "$CORP_ROOT_CA_B64" | base64 -d \
            | sudo tee /usr/local/share/ca-certificates/corp-root-ca.crt > /dev/null
          sudo update-ca-certificates
          echo 'export NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/corp-root-ca.crt' \
            | sudo tee /etc/profile.d/node-ca.sh > /dev/null

      # 4. Proxy — route traffic through corporate proxy
      - name: Configure proxy
        run: |
          cat << 'PROXY' | sudo tee /etc/profile.d/proxy.sh > /dev/null
          export http_proxy="$CORP_HTTP_PROXY"
          export https_proxy="$CORP_HTTPS_PROXY"
          export no_proxy="$CORP_NO_PROXY"
          export HTTP_PROXY="$CORP_HTTP_PROXY"
          export HTTPS_PROXY="$CORP_HTTPS_PROXY"
          export NO_PROXY="$CORP_NO_PROXY"
          PROXY
          source /etc/profile.d/proxy.sh

      # 5. Language runtimes
      - name: Install JDK 17
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openjdk-17-jdk-headless
          echo 'export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64' \
            | sudo tee /etc/profile.d/java.sh > /dev/null

      - name: Install Node.js tooling
        run: npm install -g pnpm

      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

    maintenance:
      - name: Configure git proxy
        run: |
          git config --global http.proxy "$CORP_HTTP_PROXY"
          git config --global https.proxy "$CORP_HTTPS_PROXY"

      - name: Configure Maven
        run: |
          mkdir -p ~/.m2
          cat > ~/.m2/settings.xml << EOF
          <settings>
            <mirrors>
              <mirror>
                <id>corp</id>
                <mirrorOf>*</mirrorOf>
                <url>$MAVEN_REGISTRY_URL</url>
              </mirror>
            </mirrors>
            <servers>
              <server>
                <id>corp</id>
                <username>$REGISTRY_USER</username>
                <password>$REGISTRY_PASS</password>
              </server>
            </servers>
          </settings>
          EOF

      - name: Configure npm
        run: |
          npm config set registry "$NPM_REGISTRY_URL"
          NPM_HOST=$(echo "$NPM_REGISTRY_URL" | sed 's|https\?://||;s|/.*||')
          npm config set "//${NPM_HOST}/:_authToken" "$REGISTRY_TOKEN"

      - name: Configure pip/uv
        run: |
          mkdir -p ~/.config/pip
          cat > ~/.config/pip/pip.conf << EOF
          [global]
          index-url = https://$REGISTRY_USER:$REGISTRY_PASS@${PYPI_REGISTRY_HOST}/simple
          EOF
          echo "export UV_INDEX_URL=https://$REGISTRY_USER:$REGISTRY_PASS@${PYPI_REGISTRY_HOST}/simple" \
            | sudo tee /etc/profile.d/uv-registry.sh > /dev/null
    ```

    <Info>
      **Order matters for `initialize` steps.** VPN must come first (so internal hosts are reachable), then DNS (so names resolve), then certificates (so HTTPS works), then proxy (so traffic routes correctly), and finally language runtimes (which may download from internal mirrors).
    </Info>
  </Accordion>
</AccordionGroup>

***

## Tips for writing good blueprints

* **Test commands in a session first.** Run commands manually in a Devin session before adding them to your blueprint. This is faster than waiting for a full build cycle.
* **Use `initialize` for install-once tools, `maintenance` for deps.** Anything that takes minutes to install (compilers, large binaries, global tools) belongs in `initialize`. Quick dependency commands (`npm install`, `uv sync`) go in `maintenance`.
* **Keep `maintenance` commands fast.** Aim for under 2 minutes. These run during builds and are surfaced to the agent at session start.
* **Use `$ENVRC` for environment variables.** Don't write to `.bashrc` or `.profile`. `$ENVRC` is the supported mechanism for setting variables across steps and sessions.
* **Name your steps.** The expanded form with `name` fields makes build log failures much easier to identify.
* **Use subshells for monorepos.** `(cd packages/foo && npm install)` runs in a subshell so subsequent steps aren't affected by the directory change.
* **Use `npm install`, not `npm ci`.** `npm ci` deletes `node_modules` and reinstalls from scratch, which is slow for `maintenance`.
* **Use repo secrets for sensitive values.** Configure them in the **Secrets** tab of the repository's blueprint editor instead of hardcoding in blueprints.

For syntax details, see the [Blueprint reference](/onboard-devin/environment/blueprint-reference). For troubleshooting build failures, see [Declarative configuration > Troubleshooting](/onboard-devin/environment/blueprints#troubleshooting-builds).
