Repository: eth-educators/ethstaker-deposit-cli

Issue:
Error-prone path handling [TOB-ETHSTAKER-4]
Reported by Trail of Bits from their *EthStaker Deposit CLI Security Assessment* DRAFT report presented on October 8 2024.

## Description
The load_text function looks at the stack to determine the absolute path to the current file and tries to modify it to find the internationalization text associated with the given file.

```python
# Determine path to json text
file_path_list = os.path.normpath(file_path).split(os.path.sep)
rel_path_list = file_path_list[file_path_list.index('ethstaker_deposit') + 1:]
json_path = resource_path(os.path.join(INTL_CONTENT_PATH, lang, *rel_path_list))
```

`file_path_list.index('ethstaker_deposit')` gets the index of the first occurrence of `/ethstaker_deposit/` in the absolute path to this file. If the absolute path contains multiple instances of this dir name, the path we end up operating on will be incorrect.

## Exploit Scenario
Alice has a username of `ethstaker_deposit` causing this function to treat her home directory as the root of its search for string translations. This file is unlikely to exist, causing the CLI tool to fail.

## Recommendations
Short term, determine the `rel_path_list` value from the last occurrence of the `ethstaker_deposit` string instead of from the first.

Long term, consider consolidating language translations into one mapping that does not rely on stack inspection and path manipulations.

Hints from issue comments:

Solution patch:
diff --git a/.github/release_template.md b/.github/release_template.md
new file mode 100644
index 00000000..cb7e2675
--- /dev/null
+++ b/.github/release_template.md
@@ -0,0 +1,43 @@
+[comment]: <> (This is a comment, it will not be included in the final release notes.)
+[comment]: <> (This template will be used to automatically generate release notes with the ci-build workflow.)
+[comment]: <> (The following values will be automatically replaced with generated content from the workflow.)
+[comment]: <> (`[GENERATED-RELEASE-NOTES]`: Replaced with the GitHub generated release notes.)
+[comment]: <> (`[WORKFLOW-URL]`: Replaced with the link to the workflow that generated the build.)
+[comment]: <> (`[BINARIES-TABLE]`: Replaced with the a markdown formatted table with a link to each binary download.)
+[comment]: <> (`[DOCKER-TABLE]`: Replaced with the a markdown formatted table with a link to the docker image.)
+
+# Summary
+
+`[Add a small summary here]`
+
+# Known Issues
+
+`[Remove this section if there is no known issue]`
+
+# All changes
+
+`[GENERATED-RELEASE-NOTES]`
+
+# Building process
+
+Release assets were built using Github Actions and [this workflow run](`[WORKFLOW-RUN-URL]`). You can establish the provenance of this build using [our artifact attestations](https://github.com/eth-educators/ethstaker-deposit-cli/attestations).
+
+With [the GitHub CLI](https://cli.github.com/) installed, a simple way to verify these assets is to run this command while replacing `[filename]` with the path to the downloaded asset:
+
+```console
+gh attestation verify [filename] --repo eth-educators/ethstaker-deposit-cli
+```
+
+This step requires you to be online. If you want to perform this offline, follow [these instructions from GitHub](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/verifying-attestations-offline).
+
+# Binaries
+
+`[BINARIES-TABLE]`
+
+# Docker image
+
+`[DOCKER-TABLE]`
+
+## License
+
+By downloading and using this software, you agree to the [license](LICENSE).
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 5f586fdc..97c732d8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -1,12 +1,18 @@
 name: ci-build
-run-name: ${{ github.actor }} is running ci-build
+run-name: ${{ github.actor }} is building binaries, building the docker image and drafting a release
 on:
   workflow_dispatch:
   push:
     tags:
       - v*
+
+# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
+env:
+  REGISTRY: ghcr.io
+  IMAGE_NAME: ${{ github.repository }}
+
 jobs:
-  ci-build:
+  build-binaries:
     runs-on: ${{ matrix.os }}
     permissions:
       id-token: write
@@ -26,84 +32,69 @@ jobs:
           cache: 'pip'
       - name: Setup variables (Linux & macOS)
         if: ${{ startsWith(matrix.os, 'ubuntu-') || startsWith(matrix.os, 'macos-') }}
+        env:
+          MATRIX_OS: '${{ matrix.os }}'
         run: |
           echo "PYTHONHASHSEED=42" >> "$GITHUB_ENV"
-          export SHORT_SHA=$(echo ${{ github.sha }} | cut -c -7)
+          SHORT_SHA=$(echo ${{ github.sha }} | cut -c -7)
           echo "SHORT_SHA=${SHORT_SHA}" >> "$GITHUB_ENV"
+          if [[ $MATRIX_OS == ubuntu-* ]] ;
+          then
+            BUILD_SYSTEM=linux
+            BUILD_CONFIGS_PATH=./build_configs/linux
+          fi
+          if [[ $MATRIX_OS == macos-* ]] ;
+          then
+            BUILD_SYSTEM=darwin
+            BUILD_CONFIGS_PATH=./build_configs/macos
+            brew install coreutils
+          fi
+          BUILD_ARCHITECTURE=amd64
+          if [[ $MATRIX_OS == *arm* ]] || [[ $MATRIX_OS == macos-latest ]] ;
+          then
+            BUILD_ARCHITECTURE=arm64
+          fi
+          BUILD_FILE_NAME=ethstaker_deposit-cli-${SHORT_SHA}-${BUILD_SYSTEM}-${BUILD_ARCHITECTURE}
+          mkdir "${BUILD_FILE_NAME}"
+          echo "BUILD_FILE_NAME=${BUILD_FILE_NAME}" >> "$GITHUB_ENV"
+          echo "BUILD_CONFIGS_PATH=${BUILD_CONFIGS_PATH}" >> "$GITHUB_ENV"
       - name: Setup variables (Windows)
         if: ${{ startsWith(matrix.os, 'windows-') }}
+        env:
+          MATRIX_OS: '${{ matrix.os }}'
         run: |
           echo "PYTHONHASHSEED=42" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
           $env:SHORT_SHA = "${{ github.sha }}".Substring(0, 7)
           echo ("SHORT_SHA=" + $env:SHORT_SHA) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
-      - name: Install Linux build dependencies
-        if: ${{ startsWith(matrix.os, 'ubuntu-') }}
-        run: |
-          python -m pip install --upgrade pip
-          pip install -r build_configs/linux/requirements.txt
-      - name: Install macOS build dependencies
-        if: ${{ startsWith(matrix.os, 'macos-') }}
-        run: |
-          python -m pip install --upgrade pip
-          pip install -r build_configs/linux/requirements.txt
-      - name: Install Windows build dependencies
-        if: ${{ startsWith(matrix.os, 'windows-') }}
-        run: |
-          python -m pip install --upgrade pip
-          pip install -r build_configs/windows/requirements.txt
-      - name: Build with build.spec (Linux amd64)
-        if: ${{ startsWith(matrix.os, 'ubuntu-') && !contains(matrix.os, 'arm') }}
-        run: |
-          export BUILD_FILE_NAME=ethstaker_deposit-cli-${SHORT_SHA}-linux-amd64
-          echo "BUILD_FILE_NAME=${BUILD_FILE_NAME}" >> "$GITHUB_ENV"
-          mkdir ${BUILD_FILE_NAME}
-          pyinstaller --distpath ./${BUILD_FILE_NAME} ./build_configs/linux/build.spec
-      - name: Build with build.spec (Linux arm64)
-        if: ${{ startsWith(matrix.os, 'ubuntu-') && contains(matrix.os, 'arm') }}
-        run: |
-          export BUILD_FILE_NAME=ethstaker_deposit-cli-${SHORT_SHA}-linux-arm64
-          echo "BUILD_FILE_NAME=${BUILD_FILE_NAME}" >> "$GITHUB_ENV"
-          mkdir ${BUILD_FILE_NAME}
-          pyinstaller --distpath ./${BUILD_FILE_NAME} ./build_configs/linux/build.spec
-      - name: Build with build.spec (macOS amd64)
-        if: ${{ matrix.os == 'macos-13' }}
-        run: |
-          export BUILD_FILE_NAME=ethstaker_deposit-cli-${SHORT_SHA}-darwin-amd64
-          echo "BUILD_FILE_NAME=${BUILD_FILE_NAME}" >> "$GITHUB_ENV"
-          mkdir ${BUILD_FILE_NAME}
-          pyinstaller --distpath ./${BUILD_FILE_NAME} ./build_configs/macos/build.spec
-      - name: Build with build.spec (macOS arm64)
-        if: ${{ matrix.os == 'macos-latest' }}
-        run: |
-          export BUILD_FILE_NAME=ethstaker_deposit-cli-${SHORT_SHA}-darwin-arm64
-          echo "BUILD_FILE_NAME=${BUILD_FILE_NAME}" >> "$GITHUB_ENV"
-          mkdir ${BUILD_FILE_NAME}
-          pyinstaller --distpath ./${BUILD_FILE_NAME} ./build_configs/macos/build.spec
-      - name: Build with build.spec (Windows amd64)
-        if: ${{ startsWith(matrix.os, 'windows-') }}
-        run: |
-          $env:BUILD_FILE_NAME = ("ethstaker_deposit-cli-" + $env:SHORT_SHA + "-windows-amd64")
+          if ($env:MATRIX_OS.Contains("arm")) {
+            $env:BUILD_ARCHITECTURE = "arm64"
+          }
+          else {
+            $env:BUILD_ARCHITECTURE = "amd64"
+          }
+          $env:BUILD_FILE_NAME = ("ethstaker_deposit-cli-" + $env:SHORT_SHA + "-windows-" + $env:BUILD_ARCHITECTURE)
           echo ("BUILD_FILE_NAME=" + $env:BUILD_FILE_NAME) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
           mkdir $env:BUILD_FILE_NAME
           $env:BUILD_FILE_NAME_PATH = (".\" + $env:BUILD_FILE_NAME)
           echo ("BUILD_FILE_NAME_PATH=" + $env:BUILD_FILE_NAME_PATH) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
-          pyinstaller --distpath $env:BUILD_FILE_NAME .\build_configs\windows\build.spec
-      - name: Install coreutils (macOS)
-        if: ${{ startsWith(matrix.os, 'macos-') }}
-        run: |
-          brew install coreutils
-      - name: Create archive and checksum (Linux & macOS)
+      - name: Build on Linux & macOS
         if: ${{ startsWith(matrix.os, 'ubuntu-') || startsWith(matrix.os, 'macos-') }}
         run: |
-          export ARCHIVE_FILE_NAME=${BUILD_FILE_NAME}.tar.gz
+          python -m pip install --upgrade pip
+          pip install -r "${BUILD_CONFIGS_PATH}"/requirements.txt
+          pyinstaller --distpath ./"${BUILD_FILE_NAME}" "${BUILD_CONFIGS_PATH}"/build.spec
+          export ARCHIVE_FILE_NAME="${BUILD_FILE_NAME}".tar.gz
           echo "ARCHIVE_FILE_NAME=${ARCHIVE_FILE_NAME}" >> "$GITHUB_ENV"
-          tar -zcvf ${ARCHIVE_FILE_NAME} ./${BUILD_FILE_NAME}
+          tar -zcvf "${ARCHIVE_FILE_NAME}" ./"${BUILD_FILE_NAME}"
           mkdir -p output/artifacts
-          cp ${ARCHIVE_FILE_NAME} output/artifacts
-          sha256sum ${ARCHIVE_FILE_NAME} | head -c 64 > output/artifacts/${ARCHIVE_FILE_NAME}.sha256
-      - name: Create archive and checksum (Windows)
+          cp "${ARCHIVE_FILE_NAME}" output/artifacts
+          sha256sum "${ARCHIVE_FILE_NAME}" | head -c 64 > output/artifacts/"${ARCHIVE_FILE_NAME}".sha256
+      - name: Build on Windows
         if: ${{ startsWith(matrix.os, 'windows-') }}
         run: |
+          python -m pip install --upgrade pip
+          pip install -r build_configs/windows/requirements.txt
+          pyinstaller --distpath $env:BUILD_FILE_NAME .\build_configs\windows\build.spec
           $env:ZIP_FILE_NAME = ($env:BUILD_FILE_NAME + ".zip")
           echo ("ZIP_FILE_NAME=" + $env:ZIP_FILE_NAME) | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
           Compress-Archive -Path $env:BUILD_FILE_NAME_PATH -DestinationPath $env:ZIP_FILE_NAME
@@ -111,25 +102,271 @@ jobs:
           copy $env:ZIP_FILE_NAME output\artifacts
           $env:CHECKSUM_FILE_NAME_PATH = ("output\artifacts\" + $env:ZIP_FILE_NAME + ".sha256")
           certUtil -hashfile $env:ZIP_FILE_NAME SHA256 | findstr /i /v "SHA256" | findstr /i /v "CertUtil" > $env:CHECKSUM_FILE_NAME_PATH
-      - name: Generate artifacts attestation (Linux & macOS)
-        if: ${{ startsWith(matrix.os, 'ubuntu-') || startsWith(matrix.os, 'macos-') }}
+      - name: Generate artifacts attestation
         uses: actions/attest-build-provenance@v1
         with:
           subject-path: output/artifacts/*
-      - name: Generate artifacts attestation (Windows)
-        if: ${{ startsWith(matrix.os, 'windows-') }}
-        uses: actions/attest-build-provenance@v1
-        with:
-          subject-path: output\artifacts\*
-      - name: Archive production artifacts (Linux & macOS)
-        if: ${{ startsWith(matrix.os, 'ubuntu-') || startsWith(matrix.os, 'macos-') }}
+      - name: Archive production artifacts
         uses: actions/upload-artifact@v4
         with:
           name: binary-${{ matrix.os }}-${{ github.sha }}-${{ github.run_id }}
           path: output/artifacts
-      - name: Archive production artifacts (Windows)
-        if: ${{ startsWith(matrix.os, 'windows-') }}
-        uses: actions/upload-artifact@v4
+  build-and-push-docker:
+    runs-on: ubuntu-latest
+    # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
+    permissions:
+      contents: read
+      packages: write
+      attestations: write
+      id-token: write
+    outputs:
+      metadata: ${{ steps.push.outputs.metadata }}
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+      # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
+      - name: Log in to the Container registry
+        # This pinned action came from docker/login-action@v3, releases can be found on https://github.com/docker/login-action/releases
+        uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
         with:
-          name: binary-${{ matrix.os }}-${{ github.sha }}-${{ github.run_id }}
-          path: output\artifacts
+          registry: ${{ env.REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
+      - name: Extract metadata (tags, labels) for Docker
+        id: meta
+        # This pinned action came from docker/metadata-action@v5, releases can be found on https://github.com/docker/metadata-action/releases
+        uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81
+        with:
+          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+      # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
+      # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
+      # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
+      - name: Build and push Docker image
+        id: push
+        # This pinned action came from docker/build-push-action@v6, releases can be found on https://github.com/docker/build-push-action/releases
+        uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75
+        with:
+          context: .
+          push: true
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+      
+      # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." 
+      - name: Generate artifact attestation
+        uses: actions/attest-build-provenance@v1
+        with:
+          subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
+          subject-digest: ${{ steps.push.outputs.digest }}
+          push-to-registry: true
+  create-release:
+    needs: [build-binaries, build-and-push-docker]
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+    steps:
+      - uses: actions/checkout@v4
+      - name: Download build binaries
+        uses: actions/download-artifact@v4
+        with:
+          path: assets/
+          pattern: binary-*
+      - name: Create draft release
+        uses: actions/github-script@v7
+        env:
+          DOCKER_IMAGE_METADATA: '${{ needs.build-and-push-docker.outputs.metadata }}'
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            var path = require('path');
+            var fs = require('fs');
+
+            const { DOCKER_IMAGE_METADATA, REGISTRY, IMAGE_NAME } = process.env;
+
+            const dockerMetadata = JSON.parse(DOCKER_IMAGE_METADATA);
+
+            var tagName = '';
+            var dockerVersion = '';
+            if (context.eventName == 'push') {
+              const tagRegex = /(?:refs\/tags\/)?(v\d+\.\d+\.\d+)$/;
+
+              const match = context.ref.match(tagRegex);
+              if (match) {
+                tagName = match[1];
+                dockerVersion = tagName;
+              } else {
+                core.setFailed(`Cannot extract the tag version from ref value '${context.ref}'.`);
+              }
+
+            } else if (context.eventName == 'workflow_dispatch') {
+              tagName = `dev-${context.actor}-${context.sha.substring(0, 7)}-${context.runId}`;
+              dockerVersion = dockerMetadata['containerimage.digest'].replace(':', '-');
+            } else {
+              core.setFailed(`Unhandled triggering event.`);
+            }
+
+            console.log(`Creating draft release for tag ${tagName}...`)
+
+            console.log(`tagName: ${tagName}`);
+            console.log(`context.sha: ${context.sha}`);
+
+            const { data: release } = await github.rest.repos.createRelease({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              tag_name: tagName,
+              target_commitish: context.sha,
+              draft: true,
+              generate_release_notes: true,
+            });
+
+            console.log(`Release ${release.id} created.`);
+
+            let binariesMap = new Map();
+            const emptyBinaryObject = {
+              system: null,
+              architecture: null,
+              binary_archive: null,
+              binary_archive_download_url: null,
+              binary_checksum: null,
+              binary_checksum_download_url: null,
+              attestation: null,
+            };
+
+            const windowsSystem = 'Windows';
+            const macOSSystem = 'macOS';
+            const linuxSystem = 'Linux';
+
+            const amd64Architecture = 'x86_64';
+            const arm64Architecture = 'aarch64';
+
+            binariesMap.set('windows-amd64', Object.assign({}, emptyBinaryObject, {
+              system: windowsSystem,
+              architecture: amd64Architecture,
+            }));
+            binariesMap.set('windows-arm64', Object.assign({}, emptyBinaryObject, {
+              system: windowsSystem,
+              architecture: arm64Architecture,
+            }));
+            binariesMap.set('darwin-amd64', Object.assign({}, emptyBinaryObject, {
+              system: macOSSystem,
+              architecture: amd64Architecture,
+            }));
+            binariesMap.set('darwin-arm64', Object.assign({}, emptyBinaryObject, {
+              system: macOSSystem,
+              architecture: arm64Architecture,
+            }));
+            binariesMap.set('linux-amd64', Object.assign({}, emptyBinaryObject, {
+              system: linuxSystem,
+              architecture: amd64Architecture,
+            }));
+            binariesMap.set('linux-arm64', Object.assign({}, emptyBinaryObject, {
+              system: linuxSystem,
+              architecture: arm64Architecture,
+            }));
+
+            console.log('Uploading release assets...');
+
+            const binaryPlatformRegex = /(\w+)-(\w+)(?=\.(?:zip|tar\.gz)(.sha256)?$)/;
+
+            const archivesGlobber = await glob.create('assets/*/*')
+            for await (const file of archivesGlobber.globGenerator()) {
+              console.log(`Uploading ${path.basename(file)} to the release ${release.id}`);
+
+              const fileName = path.basename(file);
+              const fileContent = fs.readFileSync(file);
+
+              const { data: asset } = await github.rest.repos.uploadReleaseAsset({
+                owner: context.repo.owner,
+                repo: context.repo.repo,
+                release_id: release.id,
+                name: fileName,
+                data: fileContent,
+              });
+
+              const match = fileName.match(binaryPlatformRegex);
+              if (match) {
+                const platform = `${match[1]}-${match[2]}`;
+
+                const binaryDetails = binariesMap.get(platform);
+
+                if (fileName.endsWith('.sha256')) {
+                  binariesMap.set(platform, Object.assign({}, binaryDetails, {
+                    binary_checksum: fileName,
+                    binary_checksum_download_url: asset.browser_download_url,
+                  }));
+                } else {
+                  binariesMap.set(platform, Object.assign({}, binaryDetails, {
+                    binary_archive: fileName,
+                    binary_archive_download_url: asset.browser_download_url,
+                  }));
+                }
+              }
+            }
+
+            const binariesTable = [
+              '| System  | Architecture | Binary             | Checksum               |',
+              '|---------|--------------|--------------------|------------------------|'
+            ];
+
+            binariesMap.forEach((details, platform) => {
+              if (
+                details.binary_archive !== null &&
+                details.binary_archive_download_url !== null &&
+                details.binary_checksum !== null &&
+                details.binary_checksum_download_url !== null
+              ) {
+                const system = details.system;
+                const architecture = details.architecture;
+                const binaryName = details.binary_archive;
+                const binaryUrl = details.binary_archive_download_url;
+                const checksumName = details.binary_checksum;
+                const checksumUrl = details.binary_checksum_download_url;
+
+                const binaryAssetUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${binaryName}`;
+                const checksumAssetUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/releases/download/${tagName}/${checksumName}`;
+
+                binariesTable.push(`| ${system} | ${architecture}       | [${binaryName}](${binaryAssetUrl}) | [sha256](${checksumAssetUrl}) |`);
+              }
+            });
+
+            const binariesTableContent = binariesTable.join('\n');
+
+            const dockerTable = [
+              '| Version | Name | Package |',
+              '|---------|------|---------|'
+            ];
+
+            dockerTable.push(`| ${dockerVersion}  | \`${REGISTRY}/${IMAGE_NAME}:${dockerVersion}\` | [Github Package](https://github.com/${context.repo.owner}/${context.repo.repo}/pkgs/container/ethstaker-deposit-cli) |`);
+
+            const dockerTableContent = dockerTable.join('\n');
+
+            const { data: workflowRun } = await github.rest.actions.getWorkflowRun({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              run_id: context.runId,
+            });
+
+            let releaseBodyTemplate = fs.readFileSync('.github/release_template.md', { encoding: 'utf8'});
+
+            console.log('Removing comments in release template...');
+
+            releaseBodyTemplate = releaseBodyTemplate.replaceAll(/^\[comment\]:\s*<>\s*\((.*?)\)\s*$/gm, '');
+            releaseBodyTemplate = releaseBodyTemplate.trim();
+
+            let releaseBody = releaseBodyTemplate.replaceAll('`[GENERATED-RELEASE-NOTES]`', release.body);
+            releaseBody = releaseBody.replaceAll('`[BINARIES-TABLE]`', binariesTableContent);
+            releaseBody = releaseBody.replaceAll('`[WORKFLOW-RUN-URL]`', workflowRun.html_url);
+            releaseBody = releaseBody.replaceAll('`[DOCKER-TABLE]`', dockerTableContent);
+
+            console.log('Updating release body with generated content and template...');
+
+            const { data: updatedRelease } = await github.rest.repos.updateRelease({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              release_id: release.id,
+              tag_name: tagName,
+              target_commitish: context.sha,
+              body: releaseBody,
+            });
+
+            console.log(`Release ${updatedRelease.id} updated. Explore it on ${updatedRelease.html_url}`);
diff --git a/.github/workflows/comment.yml b/.github/workflows/comment.yml
index b8907edf..fe75789e 100644
--- a/.github/workflows/comment.yml
+++ b/.github/workflows/comment.yml
@@ -1,6 +1,6 @@
 # Safely comment on the PR - https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/
-name: Comment on pull request
-run-name: commenting on PR by ${{ github.actor }}
+name: ci-comment
+run-name: ${{ github.actor }} is adding coverage report comment on PR
 on:
   workflow_run:
     workflows: [ "ci-runner" ]
@@ -8,11 +8,52 @@ on:
       - completed
 
 jobs:
-  comment:
+  completed-coverage:
     runs-on: ubuntu-latest
     if: >
       github.event.workflow_run.event == 'pull_request' &&
       github.event.workflow_run.conclusion == 'success'
+    outputs:
+      coverage: ${{ steps.output.outputs.coverage }}
+    steps:
+      - name: Get coverage job success
+        uses: actions/github-script@v7
+        env:
+          WORKFLOW_RUN_ID: '${{ github.event.workflow_run.id }}'
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          script: |
+            const { WORKFLOW_RUN_ID } = process.env;
+
+            core.exportVariable('COVERAGE', 'unknown');
+
+            const { data: allJobs } = await github.rest.actions.listJobsForWorkflowRun({
+              owner: context.repo.owner,
+              repo: context.repo.repo,
+              run_id: WORKFLOW_RUN_ID,
+            });
+
+            allJobs.jobs.forEach((job) => {
+              if (
+                job.name === 'coverage-combine' &&
+                job.status == 'completed' &&
+                job.conclusion == 'success'
+              ) {
+                core.exportVariable('COVERAGE', 'completed');
+              }
+            });
+      - name: Output coverage status
+        id: output
+        run: echo "coverage=${COVERAGE}" >> "$GITHUB_OUTPUT"
+  comment:
+    needs: completed-coverage
+    # Runs only if we have a completed coverage job from the ci-runner workflow
+    if: ${{ needs.completed-coverage.outputs.coverage == 'completed' }}
+    permissions:
+      issues: write
+      contents: write
+      pull-requests: write
+    runs-on: ubuntu-latest
     steps:
       - name: Download artifacts
         uses: actions/download-artifact@v4
@@ -21,7 +62,7 @@ jobs:
           run-id: ${{ github.event.workflow_run.id }}
           github-token: ${{ secrets.GITHUB_TOKEN }}
 
-      - name: 'Comment on PR'
+      - name: Comment on PR
         uses: actions/github-script@v7
         with:
           github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
deleted file mode 100644
index b3f8c3e9..00000000
--- a/.github/workflows/docker.yml
+++ /dev/null
@@ -1,65 +0,0 @@
-#
-name: ci-docker
-run-name: ${{ github.actor }} is running ci-docker
-
-# Configures this workflow to run every time a change is pushed to a tag that starts with `v`.
-on:
-  push:
-    tags:
-      - v*
-
-# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
-env:
-  REGISTRY: ghcr.io
-  IMAGE_NAME: ${{ github.repository }}
-
-# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
-jobs:
-  build-and-push-image:
-    runs-on: ubuntu-latest
-    # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
-    permissions:
-      contents: read
-      packages: write
-      attestations: write
-      id-token: write
-      # 
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-      # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
-      - name: Log in to the Container registry
-        # This pinned action came from docker/login-action@v3, releases can be found on https://github.com/docker/login-action/releases
-        uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
-        with:
-          registry: ${{ env.REGISTRY }}
-          username: ${{ github.actor }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-      # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
-      - name: Extract metadata (tags, labels) for Docker
-        id: meta
-        # This pinned action came from docker/metadata-action@v5, releases can be found on https://github.com/docker/metadata-action/releases
-        uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81
-        with:
-          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
-      # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
-      # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
-      # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
-      - name: Build and push Docker image
-        id: push
-        # This pinned action came from docker/build-push-action@v6, releases can be found on https://github.com/docker/build-push-action/releases
-        uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75
-        with:
-          context: .
-          push: true
-          tags: ${{ steps.meta.outputs.tags }}
-          labels: ${{ steps.meta.outputs.labels }}
-      
-      # This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)." 
-      - name: Generate artifact attestation
-        uses: actions/attest-build-provenance@v1
-        with:
-          subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
-          subject-digest: ${{ steps.push.outputs.digest }}
-          push-to-registry: true
-      
diff --git a/.github/workflows/runner.yml b/.github/workflows/runner.yml
index dc21d3c2..48ffdde2 100644
--- a/.github/workflows/runner.yml
+++ b/.github/workflows/runner.yml
@@ -1,7 +1,8 @@
 name: ci-runner
-run-name: ${{ github.actor }} is running ci-runner
+run-name: ${{ github.actor }} is testing and measuring code quality
 on:
   push:
+    branches: [main]
   pull_request:
     types: [opened, synchronize, labeled, unlabeled]
     branches: [main]
@@ -18,6 +19,7 @@ jobs:
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
+          pip install -r requirements.txt
           pip install -r requirements_test.txt
       - name: Run type checker
         run: python -m mypy --config-file mypy.ini -p ethstaker_deposit
@@ -31,9 +33,7 @@ jobs:
         run: npm install -g @prantlf/jsonlint@16.0.0
       - name: Validate JSON files
         run: |
-          for file in $(find ./ethstaker_deposit/intl -name "*.json"); do
-            jsonlint --no-duplicate-keys --quiet --compact $file
-          done
+          find ./ethstaker_deposit/intl -name "*.json" -exec jsonlint --no-duplicate-keys --quiet --compact {} \;
   ci-runner:
     needs: lint
     if: |
@@ -55,6 +55,7 @@ jobs:
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
+          pip install -r requirements.txt
           pip install -r requirements_test.txt
       - name: Run tests
         run: |
@@ -84,6 +85,7 @@ jobs:
         id: merge
         run: |
           python -m pip install --upgrade pip
+          pip install -r requirements.txt
           pip install -r requirements_test.txt
           coverage combine coverage*
           coverage html
@@ -96,11 +98,13 @@ jobs:
       - name: Generate text report
         run: |
           mkdir results
-          echo "Test Coverage: [Download HTML Report](${{ steps.upload-html.outputs.artifact-url }})" >> results/coverage-text-report
-          echo >> results/coverage-text-report
-          echo \`\`\` >> results/coverage-text-report
-          coverage report >> results/coverage-text-report
-          echo \`\`\` >> results/coverage-text-report
+          {
+            echo "Test Coverage: [Download HTML Report](${{ steps.upload-html.outputs.artifact-url }})" ;
+            echo ;
+            echo \`\`\` ;
+            coverage report ;
+            echo \`\`\` ;
+          } >> results/coverage-text-report
           echo ${{ github.event.number }} > results/issue_number
       - name: Upload final coverage text report
         uses: actions/upload-artifact@v4
diff --git a/docs/src/local_development.md b/docs/src/local_development.md
index 3f372ea7..c6618cc3 100644
--- a/docs/src/local_development.md
+++ b/docs/src/local_development.md
@@ -74,6 +74,7 @@ On Windows, you'll need:
 
 **To execute tests, you will need to install the test dependencies**:
 ```sh
+python3 -m pip install -r requirements.txt
 python3 -m pip install -r requirements_test.txt
 python3 -m pytest tests
 ```
diff --git a/docs/src/quick_setup.md b/docs/src/quick_setup.md
index 019b7212..dd00d204 100644
--- a/docs/src/quick_setup.md
+++ b/docs/src/quick_setup.md
@@ -29,16 +29,14 @@ For other installation options, including building with python or virtualenv and
 
 1. Make sure you have [the GitHub CLI installed](https://cli.github.com/).
 
-2. Download [the associated attestation](https://github.com/eth-educators/ethstaker-deposit-cli/attestations) for the archive you downloaded in Step 1.
-
-3. Verify the attestation file against the corresponding file but be sure to replace the contents with the exact file name:
+2. Verify the attestation against the corresponding file but be sure to replace the contents with the exact file name:
 ```sh
-gh attestation verify ethstaker_deposit-cli-*******-***.*** --owner eth-educators --bundle ./eth-educators-ethstaker-deposit-cli-attestation-*******.sigstore.json
+gh attestation verify ethstaker_deposit-cli-*******-***.*** --repo eth-educators/ethstaker-deposit-cli
 ```
 
 This step requires you to be online. If you want to perform this offline, follow [these instructions from GitHub](https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/verifying-attestations-offline).
 
-4. You should see `✓ Verification succeeded!` in the output **otherwise do not continue**.
+3. You should see `✓ Verification succeeded!` in the output **otherwise do not continue**.
 
 ## Step 3: Usage
 
diff --git a/docs/src/release_process.md b/docs/src/release_process.md
index 3d198557..6d4b5f11 100644
--- a/docs/src/release_process.md
+++ b/docs/src/release_process.md
@@ -10,64 +10,13 @@ This document is meant as a guide on how to perform and publish a new release ve
 git tag -a -m 'Version 1.0.0' v1.0.0
 git push origin v1.0.0
 ```
-5. Wait for all the build assets to be created by [the ci-build workflow](https://github.com/eth-educators/ethstaker-deposit-cli/actions/workflows/build.yml). Wait for the docker image to be created by [the ci-docker workflow](https://github.com/eth-educators/ethstaker-deposit-cli/actions/workflows/docker.yml).
-6. Download all the release assets from [the build process](https://github.com/eth-educators/ethstaker-deposit-cli/actions/workflows/build.yml) in step 5. Extract the zip files to get the actual release files. Test some of the binary assets to make sure there is no major issue with them.
-7. Draft [a new release on Github](https://github.com/eth-educators/ethstaker-deposit-cli/releases/new).
-8. Choose the tag you created at step 4.
-9. Click the *Generate release notes* button. Copy the generated content to be included later.
-10. Add an interesting release title.
-11. Use [the template below](#release-notes-template) for the content of the release notes. Fill in the different sections correctly. Make sure all the links are updated to work with the new release including the various asset links and the docker image link. Include the generated release notes from step 9 in the *All changes* section.
-12. Upload all the binary assets extracted in step 6 in the draft release binaries section.
-13. If this is not a production release, check the *Set as a pre-release* checkbox.
-14. Click the *Publish release* button.
-15. Determine a new dev version number. You can try to guess the next version number to the best of your ability. This will always be subject to change. Add a `dev` identifier to the version number to clearly indicate this is a dev version number.
-16. Update `ethstaker_deposit/__init__.py`'s `__version__` variable with a new dev version number. Commit this change to the main branch.
+5. Wait for all the build assets and the draft release to be created by [the ci-build workflow](https://github.com/eth-educators/ethstaker-deposit-cli/actions/workflows/build.yml).
+6. Open the draft release and fill in the different sections correctly.
+7. If this is not a production release, check the *Set as a pre-release* checkbox.
+8. Click the *Publish release* button.
+9. Determine a new dev version number. You can try to guess the next version number to the best of your ability. This will always be subject to change. Add a `dev` identifier to the version number to clearly indicate this is a dev version number.
+10. Update `ethstaker_deposit/__init__.py`'s `__version__` variable with a new dev version number. Commit this change to the main branch.
 
 ## Release Notes Template
 
-You can start the release notes with this template:
-
-```markdown
-# Summary
-
-This preview release contains all the changes that were made since the original fork of the [staking-deposit-cli project](https://github.com/ethereum/staking-deposit-cli/) ([fdab65d commit](https://github.com/ethereum/staking-deposit-cli/commit/fdab65d33a63632e1935e9a9235119a46e37c221)).
-
-Notable changes from the original project include:
-
-- New exit commands to create an exit message and perform a voluntary exit for your validators.
-- Multiprocessing support to increase the speed of processes that can be expanded to use more than a single thread or a single process. This helps with generating a large number of validator keys for instance.
-- Support for more recent OSes and Python versions by default.
-- A dedicated [documentation website](https://eth-educators.github.io/ethstaker-deposit-cli/).
-
-# Known Issues
-
-`[Remove this section if there is no known issue]`
-
-# All changes
-
-`[Generated release notes]`
-
-# Building process
-
-Release assets were built using Github Actions and [this workflow run](https://github.com/eth-educators/ethstaker-deposit-cli/actions/runs/10113717389). You can establish the provenance of this build using [our artifact attestations](https://github.com/eth-educators/ethstaker-deposit-cli/attestations).
-
-# Binaries
-
-| System  | Architecture | Binary             | Checksum               | Attestation           |
-|---------|--------------|--------------------|------------------------|-----------------------|
-| Windows | x86_64       | [ethstaker_deposit-cli-c840111-windows-amd64.zip](https://github.com/eth-educators/ethstaker-deposit-cli/releases/download/v0.1.0/ethstaker_deposit-cli-c840111-windows-amd64.zip) | [sha256](https://github.com/eth-educators/ethstaker-deposit-cli/releases/download/v0.1.0/ethstaker_deposit-cli-c840111-windows-amd64.zip.sha256) | [Attestation](https://github.com/eth-educators/ethstaker-deposit-cli/attestations/2242278) |
-| macOS   | x86_64       | [ethstaker_deposit-cli-c840111-darwin-amd64.tar.gz](https://github.com/eth-educators/ethstaker-deposit-cli/releases/download/v0.1.0/ethstaker_deposit-cli-c840111-darwin-amd64.tar.gz) | [sha256](https://github.com/eth-educators/ethstaker-deposit-cli/releases/download/v0.1.0/ethstaker_deposit-cli-c840111-darwin-amd64.tar.gz.sha256) | [Attestation](https://github.com/eth-educators/ethstaker-deposit-cli/attestations/2242270) |
-| macOS   | aarch64      | [ethstaker_deposit-cli-c840111-darwin-arm64.tar.gz](https://github.com/eth-educators/ethstaker-deposit-cli/releases/download/v0.1.0/ethstaker_deposit-cli-c840111-darwin-arm64.tar.gz) | [sha256](https://github.com/eth-educators/ethstaker-deposit-cli/releases/download/v0.1.0/ethstaker_deposit-cli-c840111-darwin-arm64.tar.gz.sha256) | [Attestation](https://github.com/eth-educators/ethstaker-deposit-cli/attestations/2242259) |
-| Linux   | x86_64       | [ethstaker_deposit-cli-c840111-linux-amd64.tar.gz](https://github.com/eth-educators/ethstaker-deposit-cli/releases/download/v0.1.0/ethstaker_deposit-cli-c840111-linux-amd64.tar.gz) | [sha256](https://github.com/eth-educators/ethstaker-deposit-cli/releases/download/v0.1.0/ethstaker_deposit-cli-c840111-linux-amd64.tar.gz.sha256) | [Attestation](https://github.com/eth-educators/ethstaker-deposit-cli/attestations/2242267) |
-| Linux   | aarch64      | [ethstaker_deposit-cli-c840111-linux-arm64.tar.gz](https://github.com/eth-educators/ethstaker-deposit-cli/releases/download/v0.1.0/ethstaker_deposit-cli-c840111-linux-arm64.tar.gz) | [sha256](https://github.com/eth-educators/ethstaker-deposit-cli/releases/download/v0.1.0/ethstaker_deposit-cli-c840111-linux-arm64.tar.gz.sha256) | [Attestation](https://github.com/eth-educators/ethstaker-deposit-cli/attestations/2242273) |
-
-# Docker image
-
-| Version | Name | Package |
-|---------|------|---------|
-| v0.1.0  | `ghcr.io/eth-educators/ethstaker-deposit-cli:v0.1.0` | [Github Package](https://github.com/eth-educators/ethstaker-deposit-cli/pkgs/container/ethstaker-deposit-cli/249338184?tag=v0.1.0) |
-
-## License
-
-By downloading and using this software, you agree to the [license](LICENSE).
-```
\ No newline at end of file
+You can find the latest release notes template on https://github.com/eth-educators/ethstaker-deposit-cli/blob/main/.github/release_template.md .
\ No newline at end of file
diff --git a/ethstaker_deposit/bls_to_execution_change_keystore.py b/ethstaker_deposit/bls_to_execution_change_keystore.py
index 78c73f7c..3aebbb6e 100644
--- a/ethstaker_deposit/bls_to_execution_change_keystore.py
+++ b/ethstaker_deposit/bls_to_execution_change_keystore.py
@@ -13,6 +13,9 @@
     compute_signing_root,
     compute_bls_to_execution_change_keystore_domain,
 )
+from ethstaker_deposit.utils.file_handling import (
+    sensitive_opener,
+)
 
 
 def bls_to_execution_change_keystore_generation(
@@ -64,8 +67,6 @@ def export_bls_to_execution_change_keystore_json(folder: str,
         'bls_to_execution_change_keystore_signature-%s-%i.json' % (index, timestamp)
     )
 
-    with open(filefolder, 'w') as f:
+    with open(filefolder, 'w', encoding='utf-8', opener=sensitive_opener) as f:
         json.dump(signed_bls_to_execution_change_keystore_json, f)
-    if os.name == 'posix':
-        os.chmod(filefolder, int('440', 8))  # Read for owner & group
     return filefolder
diff --git a/ethstaker_deposit/cli/existing_mnemonic.py b/ethstaker_deposit/cli/existing_mnemonic.py
index 0d4b44d2..8c3b53ed 100644
--- a/ethstaker_deposit/cli/existing_mnemonic.py
+++ b/ethstaker_deposit/cli/existing_mnemonic.py
@@ -10,6 +10,7 @@
 from ethstaker_deposit.key_handling.key_derivation.mnemonic import (
     reconstruct_mnemonic,
 )
+from ethstaker_deposit.utils import config
 from ethstaker_deposit.utils.constants import (
     MNEMONIC_LANG_OPTIONS,
     WORD_LISTS_PATH,
@@ -118,6 +119,8 @@ def existing_mnemonic(ctx: click.Context, mnemonic: str, mnemonic_password: str,
     ctx.obj.update({'mnemonic': mnemonic, 'mnemonic_password': mnemonic_password})
     # Clear clipboard
     try:  # Failing this on headless Linux is expected
+        if not config.non_interactive:
+            click.pause(load_text(['msg_confirm_clipboard_clearing']))
         pyperclip.copy(' ')
     except Exception:
         pass
diff --git a/ethstaker_deposit/cli/new_mnemonic.py b/ethstaker_deposit/cli/new_mnemonic.py
index 12bbe71a..e4c70c62 100644
--- a/ethstaker_deposit/cli/new_mnemonic.py
+++ b/ethstaker_deposit/cli/new_mnemonic.py
@@ -53,15 +53,26 @@ def new_mnemonic(ctx: click.Context, mnemonic_language: str, **kwargs: Any) -> N
     test_mnemonic = ''
     while mnemonic != reconstruct_mnemonic(test_mnemonic, WORD_LISTS_PATH, mnemonic_language):
         clear_terminal()
-        click.echo(load_text(['msg_mnemonic_presentation']))
+        click.echo(
+            load_text(['msg_mnemonic_presentation'])
+            + '\n\n********************\n'
+            + load_text(['msg_mnemonic_clipboard_warning'])
+            + '\n********************'
+        )
         click.echo('\n\n%s\n\n' % mnemonic)
         click.pause(load_text(['msg_press_any_key']))
 
         clear_terminal()
-        test_mnemonic = click.prompt(load_text(['msg_mnemonic_retype_prompt']) + '\n\n')
+        test_mnemonic = click.prompt(
+            load_text(['msg_mnemonic_retype_prompt'])
+            + '\n\n********************\n'
+            + load_text(['msg_mnemonic_clipboard_warning'])
+            + '\n********************\n\n'
+        )
     clear_terminal()
     # Clear clipboard
     try:  # Failing this on headless Linux is expected
+        click.pause(load_text(['msg_confirm_clipboard_clearing']))
         pyperclip.copy(' ')
     except Exception:
         pass
diff --git a/ethstaker_deposit/credentials.py b/ethstaker_deposit/credentials.py
index f282b1fc..8bc2e757 100644
--- a/ethstaker_deposit/credentials.py
+++ b/ethstaker_deposit/credentials.py
@@ -38,6 +38,9 @@
     DepositMessage,
     SignedBLSToExecutionChange,
 )
+from ethstaker_deposit.utils.file_handling import (
+    sensitive_opener,
+)
 
 
 class WithdrawalType(Enum):
@@ -364,8 +367,6 @@ def export_bls_to_execution_change_json(self, folder: str, validator_indices: Se
                     bar.update(1)
 
         filefolder = os.path.join(folder, 'bls_to_execution_change-%i.json' % time.time())
-        with open(filefolder, 'w') as f:
+        with open(filefolder, 'w', encoding='utf-8', opener=sensitive_opener) as f:
             json.dump(bls_to_execution_changes, f)
-        if os.name == 'posix':
-            os.chmod(filefolder, int('440', 8))  # Read for owner & group
         return filefolder
diff --git a/ethstaker_deposit/intl/en/cli/existing_mnemonic.json b/ethstaker_deposit/intl/en/cli/existing_mnemonic.json
index 2075e221..43386090 100644
--- a/ethstaker_deposit/intl/en/cli/existing_mnemonic.json
+++ b/ethstaker_deposit/intl/en/cli/existing_mnemonic.json
@@ -24,6 +24,7 @@
             "help": "Enter the index (key number) you wish to start generating more keys from. For example, if you've generated 4 keys in the past, you'd enter 4 here.",
             "prompt": "Enter the index (key number) you wish to start generating more keys from. For example, if you've generated 4 keys in the past, you'd enter 4 here.",
             "confirm": "Please repeat the validator start index to confirm"
-        }
+        },
+        "msg_confirm_clipboard_clearing": "WARNING: Your clipboard will be CLEARED. Press any key to clear the clipboard."
     }
 }
diff --git a/ethstaker_deposit/intl/en/cli/new_mnemonic.json b/ethstaker_deposit/intl/en/cli/new_mnemonic.json
index ec6e2aa7..042e2e08 100644
--- a/ethstaker_deposit/intl/en/cli/new_mnemonic.json
+++ b/ethstaker_deposit/intl/en/cli/new_mnemonic.json
@@ -10,6 +10,8 @@
         },
         "msg_mnemonic_presentation": "This is your mnemonic (seed phrase). Write it down and store it safely. It is the ONLY way to retrieve your deposit.",
         "msg_press_any_key": "Press any key when you have written down your mnemonic.",
-        "msg_mnemonic_retype_prompt": "Please type your mnemonic (separated by spaces) to confirm you have written it down. You MUST write the mnemonic down, as the clipboard will be cleared after this step. Note: you only need to enter the first 4 letters of each word if you'd prefer."
+        "msg_mnemonic_retype_prompt": "Please type your mnemonic (separated by spaces) to confirm you have written it down. Note: you only need to enter the first 4 letters of each word if you'd prefer.",
+        "msg_mnemonic_clipboard_warning": "WARNING: You MUST write the mnemonic down, as the clipboard will be cleared after this step.",
+        "msg_confirm_clipboard_clearing": "WARNING: Your clipboard will be CLEARED. Press any key to clear the clipboard."
     }
 }
diff --git a/ethstaker_deposit/key_handling/keystore.py b/ethstaker_deposit/key_handling/keystore.py
index f5dc4f3c..ed90aafe 100644
--- a/ethstaker_deposit/key_handling/keystore.py
+++ b/ethstaker_deposit/key_handling/keystore.py
@@ -5,7 +5,7 @@
     field as dataclass_field
 )
 import json
-import os
+
 from py_ecc.bls import G2ProofOfPossession as bls
 from secrets import randbits
 from typing import Any, Dict, Union
@@ -21,6 +21,9 @@
 from ethstaker_deposit.utils.constants import (
     UNICODE_CONTROL_CHARS,
 )
+from ethstaker_deposit.utils.file_handling import (
+    sensitive_opener,
+)
 
 hexdigits = set('0123456789abcdef')
 
@@ -96,10 +99,8 @@ def save(self, filefolder: str) -> None:
         """
         Save self as a JSON keystore.
         """
-        with open(filefolder, 'w') as f:
+        with open(filefolder, 'w', encoding='utf-8', opener=sensitive_opener) as f:
             f.write(self.as_json())
-        if os.name == 'posix':
-            os.chmod(filefolder, int('440', 8))  # Read for owner & group
 
     @classmethod
     def from_json(cls, json_dict: Dict[Any, Any]) -> 'Keystore':
diff --git a/ethstaker_deposit/utils/deposit.py b/ethstaker_deposit/utils/deposit.py
index 2ce78aef..6482f06c 100644
--- a/ethstaker_deposit/utils/deposit.py
+++ b/ethstaker_deposit/utils/deposit.py
@@ -1,11 +1,13 @@
 import json
 import os
 
+from ethstaker_deposit.utils.file_handling import (
+    sensitive_opener,
+)
+
 
 def export_deposit_data_json(folder: str, timestamp: float, deposit_data: list[dict[str, bytes]]) -> str:
     file_folder = os.path.join(folder, 'deposit_data-%i.json' % timestamp)
-    with open(file_folder, 'w') as f:
+    with open(file_folder, 'w', encoding='utf-8', opener=sensitive_opener) as f:
         json.dump(deposit_data, f, default=lambda x: x.hex())
-    if os.name == 'posix':
-        os.chmod(file_folder, int('440', 8))  # Read for owner & group
     return file_folder
diff --git a/ethstaker_deposit/utils/exit_transaction.py b/ethstaker_deposit/utils/exit_transaction.py
index dfd7934e..71de5f64 100644
--- a/ethstaker_deposit/utils/exit_transaction.py
+++ b/ethstaker_deposit/utils/exit_transaction.py
@@ -10,6 +10,9 @@
     compute_signing_root,
     compute_voluntary_exit_domain,
 )
+from ethstaker_deposit.utils.file_handling import (
+    sensitive_opener,
+)
 
 
 def exit_transaction_generation(
@@ -55,8 +58,6 @@ def export_exit_transaction_json(folder: str, signed_exit: SignedVoluntaryExit,
         )
     )
 
-    with open(filefolder, 'w') as f:
+    with open(filefolder, 'w', encoding='utf-8', opener=sensitive_opener) as f:
         json.dump(signed_exit_json, f)
-    if os.name == 'posix':
-        os.chmod(filefolder, int('440', 8))  # Read for owner & group
     return filefolder
diff --git a/ethstaker_deposit/utils/file_handling.py b/ethstaker_deposit/utils/file_handling.py
index f0142491..dfe84a25 100644
--- a/ethstaker_deposit/utils/file_handling.py
+++ b/ethstaker_deposit/utils/file_handling.py
@@ -1,5 +1,6 @@
 import os
 import sys
+from typing import Any, Union
 
 
 def resource_path(relative_path: str) -> str:
@@ -13,3 +14,14 @@ def resource_path(relative_path: str) -> str:
     except Exception:
         base_path = os.path.abspath(".")
     return os.path.join(base_path, relative_path)
+
+
+def sensitive_opener(path: Union[str, bytes, 'os.PathLike[Any]'], flags: int) -> int:
+    """
+    Opener to be used with the open built-in function to correctly assign permissions to sensitive
+    files when created and written to for the first time.
+    """
+    if os.name == 'posix':
+        return os.open(path, flags | os.O_EXCL, 0o400)
+    else:
+        return os.open(path, flags)
diff --git a/ethstaker_deposit/utils/intl.py b/ethstaker_deposit/utils/intl.py
index 5f5b043b..e41c7860 100644
--- a/ethstaker_deposit/utils/intl.py
+++ b/ethstaker_deposit/utils/intl.py
@@ -59,7 +59,11 @@ def load_text(params: list[str], file_path: str = '', func: str = '', lang: str
 
     # Determine path to json text
     file_path_list = os.path.normpath(file_path).split(os.path.sep)
-    rel_path_list = file_path_list[file_path_list.index('ethstaker_deposit') + 1:]
+
+    # Find the relative path to the internationalization file from the project directory
+    last_occurrence = len(file_path_list) - file_path_list[::-1].index('ethstaker_deposit')
+    rel_path_list = file_path_list[last_occurrence:]
+
     json_path = resource_path(os.path.join(INTL_CONTENT_PATH, lang, *rel_path_list))
 
     try:


Test patch:
diff --git a/tests/test_cli/helpers.py b/tests/test_cli/helpers.py
index b42e170c..7271b998 100644
--- a/tests/test_cli/helpers.py
+++ b/tests/test_cli/helpers.py
@@ -60,7 +60,7 @@ def get_permissions(path: str, file_name: str) -> str:
 def verify_file_permission(os_ref, folder_path, files):
     if os_ref.name == 'posix':
         for file_name in files:
-            assert get_permissions(folder_path, file_name) == '0o440'
+            assert get_permissions(folder_path, file_name) == '0o400'
 
 
 def prepare_testing_folder(os_ref, testing_folder_name='TESTING_TEMP_FOLDER'):
diff --git a/tests/test_cli/test_existing_mnemonic.py b/tests/test_cli/test_existing_mnemonic.py
index db703e52..7252af90 100644
--- a/tests/test_cli/test_existing_mnemonic.py
+++ b/tests/test_cli/test_existing_mnemonic.py
@@ -23,7 +23,7 @@ def test_existing_mnemonic_bls_withdrawal() -> None:
     inputs = [
         'TREZOR',
         'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
-        '2', '2', '5', 'mainnet', 'MyPasswordIs', 'MyPasswordIs']
+        '2', '2', '5', 'mainnet', 'MyPasswordIs', 'MyPasswordIs', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -51,7 +51,7 @@ def test_existing_mnemonic_bls_withdrawal() -> None:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
     # Clean up
     clean_key_folder(my_folder_path)
 
@@ -68,7 +68,7 @@ def test_existing_mnemonic_withdrawal_address() -> None:
     inputs = [
         'TREZOR',
         'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
-        '2', '2', '5', 'mainnet', 'MyPasswordIs', 'MyPasswordIs', withdrawal_address, withdrawal_address]
+        '2', '2', '5', 'mainnet', 'MyPasswordIs', 'MyPasswordIs', withdrawal_address, withdrawal_address, '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -104,7 +104,7 @@ def test_existing_mnemonic_withdrawal_address() -> None:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
     # Clean up
     clean_key_folder(my_folder_path)
 
@@ -126,7 +126,7 @@ def test_existing_mnemonic_withdrawal_address_bad_checksum() -> None:
         'TREZOR',
         'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
         '2', '2', '5', 'mainnet', 'MyPasswordIs', 'MyPasswordIs',
-        wrong_withdrawal_address, correct_withdrawal_address, correct_withdrawal_address
+        wrong_withdrawal_address, correct_withdrawal_address, correct_withdrawal_address, ''
     ]
     data = '\n'.join(inputs)
     arguments = [
@@ -163,7 +163,7 @@ def test_existing_mnemonic_withdrawal_address_bad_checksum() -> None:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
     # Clean up
     clean_key_folder(my_folder_path)
 
@@ -184,7 +184,7 @@ def test_pbkdf2_new_mnemonic() -> None:
     runner = CliRunner()
     inputs = [
         'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
-        '0', '0', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs',
+        '0', '0', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs', '',
     ]
     data = '\n'.join(inputs)
     arguments = [
@@ -289,7 +289,7 @@ async def test_script() -> None:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
@@ -338,7 +338,7 @@ async def test_script_abbreviated_mnemonic() -> None:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
@@ -364,7 +364,7 @@ def test_existing_mnemonic_custom_testnet() -> None:
     inputs = [
         'TREZOR',
         'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
-        '2', '2', '5', 'MyPasswordIs', 'MyPasswordIs']
+        '2', '2', '5', 'MyPasswordIs', 'MyPasswordIs', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -393,7 +393,7 @@ def test_existing_mnemonic_custom_testnet() -> None:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
     # Clean up
     clean_key_folder(my_folder_path)
 
@@ -409,7 +409,7 @@ def test_existing_mnemonic_multiple_languages() -> None:
     inputs = [
         'TREZOR',
         '的 的 的 的 的 的 的 的 的 的 的 在', '1',
-        '2', '2', '5', 'MyPasswordIs', 'MyPasswordIs']
+        '2', '2', '5', 'MyPasswordIs', 'MyPasswordIs', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -438,7 +438,7 @@ def test_existing_mnemonic_multiple_languages() -> None:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
     # Clean up
     clean_key_folder(my_folder_path)
 
@@ -454,7 +454,7 @@ def test_existing_mnemonic_multiple_languages_argument() -> None:
     inputs = [
         'TREZOR',
         '的 的 的 的 的 的 的 的 的 的 的 在',
-        '2', '2', '5', 'MyPasswordIs', 'MyPasswordIs']
+        '2', '2', '5', 'MyPasswordIs', 'MyPasswordIs', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -484,6 +484,6 @@ def test_existing_mnemonic_multiple_languages_argument() -> None:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
     # Clean up
     clean_key_folder(my_folder_path)
diff --git a/tests/test_cli/test_new_mnemonic.py b/tests/test_cli/test_new_mnemonic.py
index 0a447705..091e4869 100644
--- a/tests/test_cli/test_new_mnemonic.py
+++ b/tests/test_cli/test_new_mnemonic.py
@@ -44,7 +44,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
 
     runner = CliRunner()
     inputs = ['english', 'english', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs',
-              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about']
+              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', '']
     data = '\n'.join(inputs)
     arguments = [
         '--ignore_connectivity',
@@ -69,7 +69,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
@@ -91,7 +91,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     runner = CliRunner()
     withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa'
     inputs = ['english', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs', withdrawal_address, withdrawal_address,
-              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about']
+              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -125,7 +125,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
@@ -152,7 +152,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
 
     inputs = ['english', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs',
               wrong_withdrawal_address, correct_withdrawal_address, correct_withdrawal_address,
-              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about']
+              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -186,7 +186,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
@@ -208,7 +208,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     runner = CliRunner()
     withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa'
     inputs = [withdrawal_address, 'english', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs',
-              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about']
+              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -243,7 +243,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
@@ -266,7 +266,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     runner = CliRunner()
     withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa'
     inputs = [withdrawal_address, 'english', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs',
-              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about']
+              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -301,7 +301,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
@@ -324,7 +324,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     runner = CliRunner()
     withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa'
     inputs = [withdrawal_address, 'english', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs',
-              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about']
+              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -359,7 +359,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
@@ -387,7 +387,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     runner = CliRunner()
 
     inputs = ['english', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs',
-              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about']
+              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -448,7 +448,8 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     clean_key_folder(scrypt_folder_path)
 
 
-@pytest.mark.skipif(sys.version_info[:2] == (3, 9) and sys.platform == "darwin", reason="breaks on macOS Python 3.9")
+@pytest.mark.skipif(sys.version_info[:2] == (3, 9), reason=(
+    "asyncio subprocess is broken in different ways on 3.9 with https://github.com/python/cpython/issues/88050"))
 @pytest.mark.asyncio
 async def test_script_bls_withdrawal() -> None:
     # Prepare folder
@@ -487,20 +488,28 @@ async def test_script_bls_withdrawal() -> None:
     )
 
     seed_phrase = ''
+    encoded_phrase = b''
     parsing = False
     mnemonic_json_file = os.path.join(os.getcwd(), 'ethstaker_deposit/../ethstaker_deposit/cli/', 'new_mnemonic.json')
+    msg_mnemonic_clipboard_warning = load_text(['msg_mnemonic_clipboard_warning'], mnemonic_json_file, 'new_mnemonic')
     async for out in proc.stdout:
         output = out.decode('utf-8').rstrip()
         if output.startswith(load_text(['msg_mnemonic_presentation'], mnemonic_json_file, 'new_mnemonic')):
             parsing = True
         elif output.startswith(load_text(['msg_mnemonic_retype_prompt'], mnemonic_json_file, 'new_mnemonic')):
-            parsing = False
+            proc.stdin.write(encoded_phrase)
+            proc.stdin.write(b'\n')
+        elif output.startswith(load_text(['msg_confirm_clipboard_clearing'], mnemonic_json_file, 'new_mnemonic')):
+            proc.stdin.write(b'\n')
         elif parsing:
-            seed_phrase += output
-            if len(seed_phrase) > 0:
-                encoded_phrase = seed_phrase.encode()
-                proc.stdin.write(encoded_phrase)
-                proc.stdin.write(b'\n')
+            if (
+                not output.startswith('********************')
+                and not output.startswith(msg_mnemonic_clipboard_warning)
+            ):
+                seed_phrase += output
+                if len(seed_phrase) > 0:
+                    encoded_phrase = seed_phrase.encode()
+                    parsing = False
 
     assert len(seed_phrase) > 0
 
@@ -530,13 +539,14 @@ async def test_script_bls_withdrawal() -> None:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
 
 
-@pytest.mark.skipif(sys.version_info[:2] == (3, 9) and sys.platform == "darwin", reason="breaks on macOS Python 3.9")
+@pytest.mark.skipif(sys.version_info[:2] == (3, 9), reason=(
+    "asyncio subprocess is broken in different ways on 3.9 with https://github.com/python/cpython/issues/88050"))
 @pytest.mark.asyncio
 async def test_script_abbreviated_mnemonic() -> None:
     # Prepare folder
@@ -575,21 +585,29 @@ async def test_script_abbreviated_mnemonic() -> None:
     )
 
     seed_phrase = ''
+    encoded_phrase = b''
     parsing = False
     mnemonic_json_file = os.path.join(os.getcwd(), 'ethstaker_deposit/../ethstaker_deposit/cli/', 'new_mnemonic.json')
+    msg_mnemonic_clipboard_warning = load_text(['msg_mnemonic_clipboard_warning'], mnemonic_json_file, 'new_mnemonic')
     async for out in proc.stdout:
         output = out.decode('utf-8').rstrip()
         if output.startswith(load_text(['msg_mnemonic_presentation'], mnemonic_json_file, 'new_mnemonic')):
             parsing = True
         elif output.startswith(load_text(['msg_mnemonic_retype_prompt'], mnemonic_json_file, 'new_mnemonic')):
-            parsing = False
+            proc.stdin.write(encoded_phrase)
+            proc.stdin.write(b'\n')
+        elif output.startswith(load_text(['msg_confirm_clipboard_clearing'], mnemonic_json_file, 'new_mnemonic')):
+            proc.stdin.write(b'\n')
         elif parsing:
-            seed_phrase += output
-            if len(seed_phrase) > 0:
-                abbreviated_mnemonic = ' '.join(abbreviate_words(seed_phrase.split(' ')))
-                encoded_phrase = abbreviated_mnemonic.encode()
-                proc.stdin.write(encoded_phrase)
-                proc.stdin.write(b'\n')
+            if (
+                not output.startswith('********************')
+                and not output.startswith(msg_mnemonic_clipboard_warning)
+            ):
+                seed_phrase += output
+                if len(seed_phrase) > 0:
+                    abbreviated_mnemonic = ' '.join(abbreviate_words(seed_phrase.split(' ')))
+                    encoded_phrase = abbreviated_mnemonic.encode()
+                    parsing = False
 
     assert len(seed_phrase) > 0
 
@@ -609,7 +627,7 @@ async def test_script_abbreviated_mnemonic() -> None:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
@@ -639,7 +657,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
 
     runner = CliRunner()
     inputs = ['english', 'english', '1', 'MyPasswordIs', 'MyPasswordIs',
-              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about']
+              'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', '']
     data = '\n'.join(inputs)
     arguments = [
         '--ignore_connectivity',
@@ -665,7 +683,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in key_files:
-            assert get_permissions(validator_keys_folder_path, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(my_folder_path)
diff --git a/tests/test_cli/test_partial_deposit.py b/tests/test_cli/test_partial_deposit.py
index 8ab9ce12..2fa84c20 100644
--- a/tests/test_cli/test_partial_deposit.py
+++ b/tests/test_cli/test_partial_deposit.py
@@ -68,7 +68,7 @@ def test_partial_deposit(amount: str) -> None:
     assert len(deposit_files) == 1
 
     if os.name == 'posix':
-        assert get_permissions(partial_deposit_folder, deposit_files[0]) == '0o440'
+        assert get_permissions(partial_deposit_folder, deposit_files[0]) == '0o400'
 
     clean_partial_deposit_folder(my_folder_path)
 
@@ -128,16 +128,16 @@ def test_partial_deposit_matches_existing_mnemonic_deposit() -> None:
 
     assert len(partial_deposit_files) == 1
 
-    with open(os.path.join(validator_key_folder, deposit_files[0]), 'r') as file1:
+    with open(os.path.join(validator_key_folder, deposit_files[0]), 'r', encoding='utf-8') as file1:
         deposit_contents = file1.read()
 
-    with open(os.path.join(partial_deposit_folder, partial_deposit_files[0]), 'r') as file2:
+    with open(os.path.join(partial_deposit_folder, partial_deposit_files[0]), 'r', encoding='utf-8') as file2:
         partial_deposit_contents = file2.read()
 
     assert deposit_contents == partial_deposit_contents
 
     if os.name == 'posix':
-        assert get_permissions(partial_deposit_folder, partial_deposit_files[0]) == '0o440'
+        assert get_permissions(partial_deposit_folder, partial_deposit_files[0]) == '0o400'
 
     clean_folder(my_folder_path, validator_key_folder, True)
     clean_partial_deposit_folder(my_folder_path)
@@ -198,10 +198,10 @@ def test_partial_deposit_does_not_match_if_amount_differs() -> None:
 
     assert len(partial_deposit_files) == 1
 
-    with open(os.path.join(validator_key_folder, deposit_files[0]), 'r') as file1:
+    with open(os.path.join(validator_key_folder, deposit_files[0]), 'r', encoding='utf-8') as file1:
         deposit_contents = file1.read()
 
-    with open(os.path.join(partial_deposit_folder, partial_deposit_files[0]), 'r') as file2:
+    with open(os.path.join(partial_deposit_folder, partial_deposit_files[0]), 'r', encoding='utf-8') as file2:
         partial_deposit_contents = file2.read()
 
     assert deposit_contents != partial_deposit_contents
@@ -218,7 +218,7 @@ def test_partial_deposit_does_not_match_if_amount_differs() -> None:
     assert deposit_contents_dict["deposit_cli_version"] == partial_deposit_contents_dict["deposit_cli_version"]
 
     if os.name == 'posix':
-        assert get_permissions(partial_deposit_folder, partial_deposit_files[0]) == '0o440'
+        assert get_permissions(partial_deposit_folder, partial_deposit_files[0]) == '0o400'
 
     clean_folder(my_folder_path, validator_key_folder, True)
     clean_partial_deposit_folder(my_folder_path)
@@ -281,7 +281,7 @@ def test_partial_deposit_custom_network(amount: str) -> None:
     assert len(deposit_files) == 1
 
     if os.name == 'posix':
-        assert get_permissions(partial_deposit_folder, deposit_files[0]) == '0o440'
+        assert get_permissions(partial_deposit_folder, deposit_files[0]) == '0o400'
 
     clean_partial_deposit_folder(my_folder_path)
 
diff --git a/tests/test_cli/test_regeneration.py b/tests/test_cli/test_regeneration.py
index 160ce145..aaa326a0 100644
--- a/tests/test_cli/test_regeneration.py
+++ b/tests/test_cli/test_regeneration.py
@@ -33,7 +33,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     runner = CliRunner()
     # Create index 0 and 1
     my_password = "MyPasswordIs"
-    inputs = ['english', 'english', '2', 'mainnet', my_password, my_password, mock_mnemonic]
+    inputs = ['english', 'english', '2', 'mainnet', my_password, my_password, mock_mnemonic, '']
     data = '\n'.join(inputs)
     arguments = [
         '--ignore_connectivity',
@@ -56,7 +56,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in part_1_key_files:
-            assert get_permissions(validator_keys_folder_path_1, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path_1, file_name) == '0o400'
 
     # Part 2: existing-mnemonic
     runner = CliRunner()
@@ -64,7 +64,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     inputs = [
         'english',
         mock_mnemonic,
-        '1', '1', '2', 'mainnet', 'MyPasswordIs', 'MyPasswordIs']
+        '1', '1', '2', 'mainnet', 'MyPasswordIs', 'MyPasswordIs', '']
     data = '\n'.join(inputs)
     arguments = [
         '--ignore_connectivity',
@@ -98,7 +98,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str:
     # Verify file permissions
     if os.name == 'posix':
         for file_name in part_2_key_files:
-            assert get_permissions(validator_keys_folder_path_2, file_name) == '0o440'
+            assert get_permissions(validator_keys_folder_path_2, file_name) == '0o400'
 
     # Clean up
     clean_key_folder(folder_path_1)
diff --git a/tests/test_deposit.py b/tests/test_deposit.py
index b98b473c..6e7efc5d 100644
--- a/tests/test_deposit.py
+++ b/tests/test_deposit.py
@@ -98,7 +98,7 @@ def _mock_socket_getaddrinfo(url, port):
     runner = CliRunner()
     inputs = [
         'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
-        '0', '0', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs']
+        '0', '0', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',
@@ -131,7 +131,7 @@ def _mock_socket_getaddrinfo(url, port):
     runner = CliRunner()
     inputs = [
         'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about',
-        '0', '0', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs']
+        '0', '0', '1', 'mainnet', 'MyPasswordIs', 'MyPasswordIs', '']
     data = '\n'.join(inputs)
     arguments = [
         '--language', 'english',