AI Code Reviews with Ollama

Review code. Stay local

Posted by Isaac on Tuesday, July 21, 2026

In recent times I have been looking closely into AI Code Reviews using tools like Github Copilot, which consumes my tokens and uses Azure/Microsoft resources. I have also looked at offerings from Qodo as well as CodeRabbit.

All these SaaS tool suites are great, no doubt, but they all cost money. I got to thinking, could I not accomplish the same thing locally?

Setup

I’ll want to use an Ollama host in my network.

Here I started a draft of this post and fed the diff to an Ollama command running on a different host:

$ OLLAMA_HOST=192.168.1.220:114344 ollama run gemma4:e4b "is this code change problematic? please give a one line summary starting with the word SUMMARY: `git diff`" | tee resp.out
2026/07/08 07:05:57 config.go:51: WARN invalid port, using default port=114344 default=11434
2026/07/08 07:05:57 config.go:51: WARN invalid port, using default port=114344 default=11434
2026/07/08 07:05:57 config.go:51: WARN invalid port, using default port=114344 default=11434
2026/07/08 07:05:57 config.go:51: WARN invalid port, using default port=114344 default=11434
Thinking...
Here's a thinking process to arrive at the suggested summary:

1.  **Analyze the Request:** The user wants to know if a provided `diff` 
(code change) is problematic and requests a one-line summary starting with 
"SUMMARY:".

2.  **Examine the Diff:**
    *   **File:** `content/post/2026-07-21-codereview.md` (A Markdown 
post).
    *   **Changes:** The changes occur near the end of a section 
discussing AI Code Reviews and their cost.
    *   **Original Content Context (Implied):** The article discusses the 
limitations (cost) of commercial AI code review tools and suggests finding 
local alternatives.
    *   **Added Lines (The Diff):**
        ```markdown
        # setup

        I'll want to use an Ollama host in my network.

        Perhaps I can test a command
        ```

3.  **Determine the Nature of the Change:**
    *   The change is *additive*. It introduces new structural elements 
(`# setup`) and preparatory text that indicates the next steps of the 
article (setting up Ollama, testing commands).
    *   This looks like an outline or a transitional placeholder for the 
technical instructions that will follow.

4.  **Evaluate Potential Problems:**
    *   *Syntax/Technical Issues:* No obvious syntax errors in Markdown.
    *   *Logic Flow:* The jump from "local alternatives" to "# setup / 
Ollama host" is a logical progression within the context of an article 
about local AI tools.
    *   *Completeness:* If this section *requires* content immediately, it 
might be incomplete. However, as a technical markdown draft, placeholders 
are common and acceptable if they frame the next required work.

5.  **Formulate the Conclusion (Is it problematic?):** No, it is not 
fundamentally problematic. It appears to be adding necessary structure for 
subsequent "how-to" steps in a blog post. The content itself is 
introductory and organizational.

6.  **Draft the One-Line Summary:**
    *   Must start with: `SUMMARY:`
    *   Needs to summarize the action (adding setup 
instructions/placeholder).
    *   (Self-Correction during drafting): I need to make sure the summary 
confirms that *if* it is a draft, it's fine, but if the article needs 
immediate polish, the placeholder nature should be mentioned.

7.  **Final Output Generation:** (Matches the provided model answer.)
...done thinking.

SUMMARY: The change is not problematic; it adds necessary structural 
placeholders (`# setup`) and introduces the technical topic of using an 
Ollama host, logically transitioning the post from discussing AI tools to 
outlining the practical local setup steps.

Now, for scripting, I would just want that summary, so we could pull that out:

$ cat resp.out | sed -n '/^SUMMARY/,$p'
SUMMARY: The change is not problematic; it adds necessary structural 
placeholders (`# setup`) and introduces the technical topic of using an 
Ollama host, logically transitioning the post from discussing AI tools to 
outlining the practical local setup steps.

So building out a pipeline, I might setup an LLM AI Check as such

name: CICD
run-name: ${{ gitea.actor }} CICD - ${{ gitea.ref_name }}
on:
  push:

env:
  OLLAMA_HOST: "192.168.1.220:11434"
  OLLAMA_MODEL: "gemma4:e4b"

jobs:
  CICD:
    runs-on: my_custom_label
    container: node:22
    steps:
      - uses: actions/checkout@v3 # Checks out your repository
        with:
          fetch-depth: 2
      - name: Install Ollama
        run: |
          apt update
          apt install -y ca-certificates curl gnupg unzip sudo groff tree jq zstd
          which ollama || true
          curl -fsSL https://ollama.com/install.sh | sh
          which ollama || true

      - name: Check Last Change
        run: |
          OLLAMA_HOST=${{ env.OLLAMA_HOST }} ollama run ${{ env.OLLAMA_MODEL }} "is this code change problematic? please give a one line summary starting with the word SUMMARY: `git diff HEAD^1 HEAD`" | tee resp.out
          echo -e "\n-----------\n"
          cat resp.out | sed -n '/^SUMMARY/,$p'
          echo -e "\n-----------\n"
          cat resp.out

In this example, we look at the last diff and give a summary. We aren’t really gating anything at this point or feeding our own rules. We’ll cover that in a bit.

/img/2026-07-ollamareview-01.png

However, in this most basic example our ollama CLI feeds the last diff to a model running on a host nearby and asks “Is this code change problematic - give me a summary”.

Pi

Ollama as the harness is a bit much. We are installing a rather large package meant to host models just to run the CLI aspect.

What if we use a much lighter harness - and one that could be installed with npm

$ npm install -g --ignore-scripts @earendil-works/pi-coding-agent
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead

added 131 packages in 4s

18 packages are looking for funding
  run `npm fund` for details

i have a model.json like this:


  "providers": {
    "ollama": {
      "baseUrl": "http://192.168.1.220:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "models": [
        { "id": "gemma4:12b" },
        { "id": "gemma4:e4b" }
      ]
    }
  }
}

I can base64 it to make it easy to move into a pipeline later.

So let’s setup the models.json as if we were in the pipeline already

$ mkdir -p ~/.pi/agent
$ echo ewogICJwcm92aWRlcnMiOiB7CiAgICAib2xsYW1hIjogewogICAgICAiYmFzZVVybCI6ICJodHRwOi8vMTkyLjE2OC4xLjIyMDoxMTQzNC92MSIsCiAgICAgICJhcGkiOiAib3BlbmFpLWNvbXBsZXRpb25zIiwKICAgICAgImFwaUtleSI6ICJvbGxhbWEiLAogICAgICAibW9kZWxzIjogWwogICAgICAgIHsgImlkIjogImdlbW1hNDoxMmIiIH0sCiAgICAgICAgeyAiaWQiOiAiZ2VtbWE0OmU0YiIgfQogICAgICBdCiAgICB9CiAgfQp9Cg== | base64 --decode > ~/.pi/agent/models.json

$ echo "tell me a joke" | pi -p "do the following: " --model gemma4:12b
It looks like you didn't include any text after the colon! If you provide a topic or a specific scenario, I can definitely craft a joke for you. 

In the meantime, here’s one for you:
**Why did the developer go broke?**
Because he kept spending all his "cents" on "cache"!

I can now assume we would want to feed in the git changes

$ git diff HEAD~1 HEAD | pi -p "evaluate this git diff and reply with a one-line summary if it is likely problematic or not: " --model gemma4:e4b
SUMMARY: The changes improve shell script cleanup and logging readability but introduce a syntactic error by appending non-shell instructional text directly into the final `run` block, causing the workflow step to fail.

Let’s use this in our Forgejo pipeline:

name: CICD
run-name: ${{ gitea.actor }} CICD - ${{ gitea.ref_name }}
on:
  push:

env:
  OLLAMA_HOST: "192.168.1.220:11434"
  OLLAMA_MODEL: "gemma4:e4b"

jobs:
  CICD:
    runs-on: my_custom_label
    container: node:22
    steps:
      - uses: actions/checkout@v3 # Checks out your repository
        with:
          fetch-depth: 2
      - name: Install Pi
        run: |
          npm install -g --ignore-scripts @earendil-works/pi-coding-agent
          mkdir -p ~/.pi/agent
          echo ewogICJwcm92aWRlcnMiOiB7CiAgICAib2xsYW1hIjogewogICAgICAiYmFzZVVybCI6ICJodHRwOi8vMTkyLjE2OC4xLjIyMDoxMTQzNC92MSIsCiAgICAgICJhcGkiOiAib3BlbmFpLWNvbXBsZXRpb25zIiwKICAgICAgImFwaUtleSI6ICJvbGxhbWEiLAogICAgICAibW9kZWxzIjogWwogICAgICAgIHsgImlkIjogImdlbW1hNDoxMmIiIH0sCiAgICAgICAgeyAiaWQiOiAiZ2VtbWE0OmU0YiIgfQogICAgICBdCiAgICB9CiAgfQp9Cg== | base64 --decode > ~/.pi/agent/models.json

      - name: Check Last Change
        run: |
          set -x
          which pi
          pi -p --model gemma4:e4b "evaluate this git diff and reply with a one-line summary if it is likely problematic or not: `git diff HEAD^1 HEAD`"

Here we can not only see the output, but how fast the pipeline ran (36s total).

/img/2026-07-ollamareview-02.png

Azure AI

I often use OpenAI models via Azure AI foundry as I can control costs by using a smaller nano deployment

I’ll set an OpenAI key

/img/2026-07-ollamareview-03.png

Now, I had many challenges trying to get pi to properly work with the deployed GPT 5 Nano instance I had - it was a mix of API versions and “responses” version “chat completions” that kept blocking it.

However, I paused and thought about it. I really don’t need a harness to just send a query to OpenAI (via Azure) and hear its response. I just need curl.

name: CICD
run-name: ${{ gitea.actor }} CICD - ${{ gitea.ref_name }}
on:
  push:

jobs:
  CICD:
    runs-on: my_custom_label
    container: node:22
    steps:
      - uses: actions/checkout@v3 # Checks out your repository
        with:
          fetch-depth: 2
      - name: Install jq
        run: |
          apt update
          apt install -y ca-certificates curl gnupg unzip sudo groff tree jq

      - name: Check Last Change
        run: |
          set -x
          #!/bin/bash

          git diff HEAD^1 HEAD > diff.txt
          FILE_PATH="diff.txt"
          ENDPOINT="https://isaac-mgp1gfv5-eastus2.cognitiveservices.azure.com/openai/deployments/gpt-5-nano/chat/completions?api-version=2025-01-01-preview"

          FILE_CONTENT=$(cat "$FILE_PATH")
          PROMPT="evaluate this git diff and reply with a one-line summary if it is likely problematic or not:\n\n$FILE_CONTENT"

          JSON_PAYLOAD=$(jq -n \
            --arg prompt_text "$PROMPT" \
            '{
              "messages": [
                {
                  "role": "user",
                  "content": $prompt_text
                }
              ],
              "max_completion_tokens": 16384,
              "model": "gpt-5-nano"
            }')

          curl -X POST "$ENDPOINT" \
              -H "Content-Type: application/json" \
              -H "Authorization: Bearer $AZURE_API_KEY" \
              -d "$JSON_PAYLOAD" | tee output.json

          echo -e "\n---------------\n"
          cat output.json | jq .choices[0].message.content

        env: # Or as an environment variable
          AZURE_API_KEY: ${{ secrets.AZOPENAIAPIKEY }}

The meat of the above is really the JSON payload out that sends a USER message with the content baked in, then the curl that sends the OpenAI data back (which is in JSON format) and lastly the jq that pulls out our evaluation

/img/2026-07-ollamareview-04.png

Perhaps we want to gate our flow. Stop right away if there is an issue found.

Let’s tweak the prompt to call that out in a keyword for which we could check:

   FILE_CONTENh=$(cat "$FILE_PATH")
   PROMPT="evaluate this git diff and reply with a one-line summary
   if it is likely problematic or not.  Make sure to say in all
   capitals PROBLEM or NOT_PROBLEM:\n\n$FILE_CONTENT"

Let’s do that - set an environment variable that would denote “good” or “bad” changes (really safe or review-more would be more appropriate words)

name: CICD
run-name: ${{ gitea.actor }} CICD - ${{ gitea.ref_name }}
on:
  push:

jobs:
  CICD:
    runs-on: my_custom_label
    container: node:22
    steps:
      - uses: actions/checkout@v3 # Checks out your repository
        with:
          fetch-depth: 2
      - name: Install jq
        run: |
          apt update
          apt install -y ca-certificates curl gnupg unzip sudo groff tree jq

      - name: Check Last Change
        id: ai_check
        shell: bash
        run: |
          set -x
          #!/bin/bash

          git diff HEAD^1 HEAD > diff.txt
          FILE_PATH="diff.txt"
          ENDPOINT="https://isaac-mgp1gfv5-eastus2.cognitiveservices.azure.com/openai/deployments/gpt-5-nano/chat/completions?api-version=2025-01-01-preview"

          FILE_CONTENT=$(cat "$FILE_PATH")
          PROMPT="evaluate this git diff and reply with a one-line summary if it is likely problematic or not.  Make sure to say in all capitals PROBLEM or NOT_PROBLEM:\n\n$FILE_CONTENT"

          JSON_PAYLOAD=$(jq -n \
            --arg prompt_text "$PROMPT" \
            '{
              "messages": [
                {
                  "role": "user",
                  "content": $prompt_text
                }
              ],
              "max_completion_tokens": 16384,
              "model": "gpt-5-nano"
            }')

          curl -X POST "$ENDPOINT" \
              -H "Content-Type: application/json" \
              -H "Authorization: Bearer $AZURE_API_KEY" \
              -d "$JSON_PAYLOAD" | tee output.json

          echo -e "\n---------------\n"
          cat output.json | jq .choices[0].message.content

          if [[ $(cat output.json | jq .choices[0].message.content) == *"NOT_PROBLEM"* ]]; then
            echo "No problem detected"
            echo "AICHECK=good" >> $GITHUB_OUTPUT
          else
            echo "Problem detected"
            echo "AICHECK=bad" >> $GITHUB_OUTPUT
            exit 1
          fi
        env: # Or as an environment variable
          AZURE_API_KEY: ${{ secrets.AZOPENAIAPIKEY }}

/img/2026-07-ollamareview-05.png

maybe we want to key off that and not just stop at the step

          if [[ $(cat output.json | jq .choices[0].message.content) == *"NOT_PROBLEM"* ]]; then
            echo "No problem detected"
            echo "AICHECK=good" >> $GITHUB_OUTPUT
          else
            echo "Problem detected"
            echo "AICHECK=bad" >> $GITHUB_OUTPUT
          fi
        env: # Or as an environment variable
          AZURE_API_KEY: ${{ secrets.AZOPENAIAPIKEY }}

      - name: Do Deployments or Packaging
        shell: bash
        run: |
          echo "We could do deployments or packaging here"
        if: ${{ steps.ai_check.outputs.AICHECK == 'good' }}

      - name: Punish the user. Bad coder. bad
        shell: bash
        run: |
          echo "We could alert the user with issues here"
        if: ${{ steps.ai_check.outputs.AICHECK == 'bad' }}

/img/2026-07-ollamareview-06.png

I, as author, might scroll up to see what OpenAI was raising issues on with my change:

/img/2026-07-ollamareview-07.png

Now all this is just using curl and jq.

And because I’m using such a small older model, it’s hard to even dig up likely costs because in the last 30 days its cost me about 7c

/img/2026-07-ollamareview-08.png

Summary and Next steps

This was just to prove the point - we can get a whole lot done with just some basic coding. I really only used AI for the above when I was trying to fight Pi and make it use the Azure AI Foundry endpoint, otherwise these are really quite simple pipeline steps anyone can throw into their flow.

And using a small model just to review a GIT diff is appropriate. We aren’t doing deeply nested analysis. If you want strong code testing use… testing. I mean, that is what unit tests and test automation frameworks are for.

The goal with an AI agent in the mix is just some quick cursory feedback.

As far as next steps go, I would next want to add in some company created Skills that detail rules around our coding best practices. Things like naming conventions, creating unit tests, documentation, code comments, tab structure (though linters are better for that).