diff --git a/.changeset/stupid-poems-glow.md b/.changeset/stupid-poems-glow.md
new file mode 100644
index 00000000000..bdd8acc66d5
--- /dev/null
+++ b/.changeset/stupid-poems-glow.md
@@ -0,0 +1,5 @@
+---
+"chainlink": minor
+---
+
+#internal rework operator_ui installer
diff --git a/.gitignore b/.gitignore
index f17a6bf1430..4f07e40516c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,7 @@ tools/clroot/db.sqlite3-wal
 *.iml
 debug.env
 *.txt
+operator_ui/install
 
 # codeship
 *.aes
@@ -102,4 +103,3 @@ override*.toml
 .venv/
 
 ocr_soak_report.csv
-
diff --git a/GNUmakefile b/GNUmakefile
index 48e15e39fb4..589e750289f 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -83,7 +83,7 @@ docker-plugins:
 
 .PHONY: operator-ui
 operator-ui: ## Fetch the frontend
-	go generate ./core/web
+	go run operator_ui/install.go .
 
 .PHONY: abigen
 abigen: ## Build & install abigen.
diff --git a/core/web/middleware.go b/core/web/middleware.go
index 17bd7e65eba..6e9378e618f 100644
--- a/core/web/middleware.go
+++ b/core/web/middleware.go
@@ -21,7 +21,7 @@ import (
 // inside this module. To achieve this, we direct webpack to output all of the compiled assets
 // in this module's folder under the "assets" directory.
 
-//go:generate ../../operator_ui/install.sh
+//go:generate go run ../../operator_ui/install.go  ../..
 //go:embed "assets"
 var uiEmbedFs embed.FS
 
diff --git a/operator_ui/README.md b/operator_ui/README.md
index 07bda2cd1dc..ccb5173631a 100644
--- a/operator_ui/README.md
+++ b/operator_ui/README.md
@@ -12,7 +12,12 @@ This package is responsible for rendering the UI of the chainlink node, which al
 
 ### Requirements
 
-The `install.sh` script handles installing the specified tag of operator UI within the [tag file](./TAG). When executed, it downloads then moves the static assets of operator UI into the `core/web/assets` path. Then, when the chainlink binary is built, these assets are included into the build that gets served.
+The `install.go` script handles installing the specified tag of operator UI within the [tag file](./TAG). When executed, it downloads then moves the static assets of operator UI into the `core/web/assets` path. Then, when the chainlink binary is built, these assets are included into the build that gets served.
+
+```sh
+# The argument is the path from the this directory to the root of the repository 
+go run ./install.go .. 
+```
 
 ## Updates
 
diff --git a/operator_ui/install.go b/operator_ui/install.go
new file mode 100644
index 00000000000..1e09783db66
--- /dev/null
+++ b/operator_ui/install.go
@@ -0,0 +1,162 @@
+package main
+
+import (
+	"archive/tar"
+	"compress/gzip"
+	"context"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+	"time"
+)
+
+func main() {
+	const (
+		owner                  = "smartcontractkit"
+		repo                   = "operator-ui"
+		fullRepo               = owner + "/" + repo
+		tagPath                = "operator_ui/TAG"
+		unpackDir              = "core/web/assets"
+		downloadTimeoutSeconds = 10
+	)
+	// Grab first argument as root directory
+	if len(os.Args) < 2 {
+		log.Fatalln("Usage: install.go <root>")
+	}
+	rootDir := os.Args[1]
+
+	tag := mustReadTagFile(path.Join(rootDir, tagPath))
+	strippedTag := stripVersionFromTag(tag)
+	assetName := fmt.Sprintf("%s-%s-%s.tgz", owner, repo, strippedTag)
+	downloadUrl := fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", fullRepo, tag, assetName)
+
+	// Assuming that we're in "root/operator_ui/"
+	unpackPath := filepath.Join(rootDir, unpackDir)
+	err := rmrf(unpackPath)
+	if err != nil {
+		log.Fatalln(err)
+	}
+
+	subPath := "package/artifacts/"
+	mustDownloadSubAsset(downloadUrl, downloadTimeoutSeconds, unpackPath, subPath)
+}
+
+func mustReadTagFile(file string) string {
+	tagBytes, err := os.ReadFile(file)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	tag := string(tagBytes)
+	return strings.TrimSpace(tag)
+}
+
+func stripVersionFromTag(tag string) string {
+	return strings.TrimPrefix(tag, "v")
+}
+
+func rmrf(path string) error {
+	err := os.RemoveAll(path)
+	if err != nil {
+		return err
+	}
+
+	err = os.Mkdir(path, 0755)
+	return err
+}
+
+// Download a sub asset from a .tgz file and extract it to a destination path
+func mustDownloadSubAsset(downloadUrl string, downloadTimeoutSeconds int, unpackPath string, subPath string) {
+	fmt.Println("Downloading", downloadUrl)
+	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(downloadTimeoutSeconds)*time.Second)
+	defer cancel()
+	/* #nosec G107 */
+	req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil)
+	if err != nil {
+		log.Fatalln(err)
+	}
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		log.Fatalln(err)
+	}
+
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		log.Fatalln(fmt.Errorf("failed to fetch asset: %s", resp.Status))
+	}
+
+	err = decompressTgzSubpath(resp.Body, unpackPath, subPath)
+	if err != nil {
+		log.Fatalln(err)
+	}
+}
+
+// Decompress a .tgz file to a destination path, only extracting files that are in the subpath
+//
+// Subpath files are extracted to the root of the destination path, rather than preserving the subpath
+func decompressTgzSubpath(file io.Reader, destPath string, subPath string) error {
+	// Create a gzip reader
+	gzr, err := gzip.NewReader(file)
+	if err != nil {
+		return fmt.Errorf("failed to create gzip reader: %w", err)
+	}
+	defer gzr.Close()
+
+	// Create a tar reader
+	tr := tar.NewReader(gzr)
+
+	// Iterate through the files in the tar archive
+	for {
+		header, err := tr.Next()
+		switch {
+		case err == io.EOF:
+			return nil // End of tar archive
+		case err != nil:
+			return fmt.Errorf("failed to read tar file: %w", err)
+		case header == nil:
+			continue
+		}
+		// skip files that arent in the subpath
+		if !strings.HasPrefix(header.Name, subPath) {
+			continue
+		}
+
+		// Strip the subpath from the header name
+		header.Name = strings.TrimPrefix(header.Name, subPath)
+
+		// Target location where the dir/file should be created
+		target := fmt.Sprintf("%s/%s", destPath, header.Name)
+
+		// Check the file type
+		switch header.Typeflag {
+		case tar.TypeDir: // Directory
+			if err := os.MkdirAll(target, 0755); err != nil {
+				return fmt.Errorf("failed to create directory: %w", err)
+			}
+			fmt.Println("Creating directory", target)
+		case tar.TypeReg: // Regular file
+			if err := writeFile(target, header, tr); err != nil {
+				return err
+			}
+		}
+	}
+}
+
+func writeFile(target string, header *tar.Header, tr *tar.Reader) error {
+	/* #nosec G110 */
+	f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
+	if err != nil {
+		return fmt.Errorf("failed to open file: %w", err)
+	}
+	defer f.Close()
+
+	if _, err := io.Copy(f, tr); err != nil {
+		return fmt.Errorf("failed to write file: %w", err)
+	}
+	fmt.Println("Creating file", target)
+	return nil
+}
diff --git a/operator_ui/install.sh b/operator_ui/install.sh
deleted file mode 100755
index f86c9a2f352..00000000000
--- a/operator_ui/install.sh
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env bash
-set -e
-
-owner=smartcontractkit
-repo=operator-ui
-fullRepo=${owner}/${repo}
-gitRoot="$(dirname -- "$0")/../"
-cd "$gitRoot/operator_ui"
-unpack_dir="$gitRoot/core/web/assets"
-tag=$(cat TAG)
-# Remove the version prefix "v"
-strippedTag="${tag:1}"
-# Taken from https://github.com/kennyp/asdf-golang/blob/master/lib/helpers.sh
-msg() {
-  echo -e "\033[32m$1\033[39m" >&2
-}
-
-err() {
-  echo -e "\033[31m$1\033[39m" >&2
-}
-
-fail() {
-  err "$1"
-  exit 1
-}
-
-msg "Getting release $tag for $fullRepo"
-# https://docs.github.com/en/rest/releases/releases#get-a-release-by-tag-name
-asset_name=${owner}-${repo}-${strippedTag}.tgz
-download_url=https://github.com/${fullRepo}/releases/download/${tag}/${asset_name}
-
-# Inspired from https://github.com/kennyp/asdf-golang/blob/master/bin/download#L29
-msg "Download URL: ${download_url}"
-# Check if we're able to download first
-http_code=$(curl -LIs -w '%{http_code}' -o /dev/null "$download_url")
-if [ "$http_code" -eq 404 ] || [ "$http_code" -eq 403 ]; then
-  fail "URL: ${download_url} returned status ${http_code}"
-fi
-# Then go ahead if we get a success code
-msg "Downloading ${fullRepo}:${tag} asset: $asset_name..."
-msg ""
-curl -L -o "$asset_name" "$download_url"
-
-msg "Unpacking asset $asset_name"
-tar -xvzf "$asset_name"
-
-msg ""
-msg "Removing old contents of $unpack_dir"
-rm -rf "$unpack_dir"
-msg "Copying contents of package/artifacts to $unpack_dir"
-cp -rf package/artifacts/. "$unpack_dir" || true
-
-msg "Cleaning up"
-rm -r package
-rm "$asset_name"