Getting Wi-Fi working on a 2015 MacBook Air with Ubuntu 26.04

I installed Ubuntu 26.04 on my early 2015 13” MacBook Air. Getting Wi-Fi working took a bit of fiddling, so I’m writing down what worked before I forget.

The laptop has a Broadcom BCM4360 wireless card. Ubuntu could see the hardware, but there was no Wi-Fi interface available.

Which driver has the card?

The useful starting point was lspci:

lspci -nn | grep -i broadcom
lspci -k | grep -A4 -i network

Initially, the driver information showed:

Kernel driver in use: bcma-pci-bridge
Kernel modules: bcma, wl

Later, it showed the available modules without a driver in use:

Kernel modules: bcma, wl

Seeing wl listed under “Kernel modules” doesn’t mean it’s actually driving the card. That was a useful distinction while figuring out what was going on.

The configuration that worked used the proprietary Broadcom STA driver, wl.

Getting enough internet to fix the internet

First, I needed a temporary connection to install the packages. iPhone USB tethering or Ethernet gets around the missing Wi-Fi.

I enabled the restricted repository and refreshed the package lists:

sudo add-apt-repository -y restricted
sudo apt update

Then removed the b43 firmware packages:

sudo apt purge -y b43-fwcutter firmware-b43-installer

And installed the STA driver:

sudo apt install -y broadcom-sta-dkms

Blacklisting the other modules

I also explicitly blacklisted b43 and bcma:

cat <<EOF | sudo tee /etc/modprobe.d/broadcom.conf
blacklist b43
blacklist bcma
EOF

Removing the firmware packages doesn’t remove these kernel modules, so the purge and blacklist do different jobs.

The STA package may already provide equivalent blacklist rules. I didn’t test whether my extra file was necessary; this is the sequence I used to get the machine working.

Then I rebuilt the initramfs and rebooted:

sudo update-initramfs -u
sudo reboot

After that, Wi-Fi appeared and worked. It also kept working through subsequent reboots.

Checking the result

These commands were useful while sorting it out:

# Which driver is using the wireless card?
lspci -k | grep -A4 -i network

# Which Broadcom modules are loaded?
lsmod | grep -E 'wl|bcma|b43'

# Does NetworkManager have a Wi-Fi device?
nmcli device

Look for wl as the driver in use and a wireless device in NetworkManager. A module appearing in the list of possible drivers isn’t enough.

Do it again after installing

I needed the fix in the live installer as well as the installed system.

Getting Wi-Fi working in the live session let me continue with the installation, but those changes didn’t carry over. I repeated the setup after booting into the installed copy of Ubuntu.

This worked on my BCM4360-equipped MacBook Air. If you’re trying it on another Mac, check the actual wireless chipset first—different models use different cards.

For mine, lspci was the useful starting point: Ubuntu could see the card, and checking which driver had claimed it gave me something concrete to investigate.

Getting Wi-Fi working on a 2015 MacBook Air with Ubuntu 26.04

Dusting off this blog

I set this blog up in 2014. In my post about the setup, I wrote about Jekyll, custom permalinks, a Makefile, and getting Travis to build it.

Coming back to it twelve years later, the Markdown and templates were still pretty easy to follow. The Ruby dependencies were showing their age.

I’ve been writing again, so it seemed like a good time to tidy things up. I also wanted a bit more confidence that changing a dependency or fiddling with a template wouldn’t break the live site.

Getting it building again

We started with GitHub Actions, updating Ruby and Bundler and getting a working lockfile committed.

There was some housekeeping along the way. The Travis configuration went, as did the reference to it on the About page. Dependabot had been complaining that it couldn’t update its own pull requests because the Bundler version was too old. Updating the lockfile sorted that out.

The Makefile is still there. The reason I gave for it in 2014 was that I’m not a Ruby or web developer and would forget the commands. I don’t think that needs revising.

Does it actually render?

A successful Jekyll build is useful, but it doesn’t tell you much about the result.

We added a Playwright test that opens the generated site in Chromium at desktop and phone sizes. It visits the posts, the About and Archive pages, pagination, and the 404 page. It checks that there’s content, the CSS loads, and the internal links work.

It also saves screenshots from each page as a build artifact. Those are handy when a test says everything is fine but you’d still like to have a look.

The link checks found that some navigation links used extensionless URLs while the generated files ended in .html. Jekyll was quite happy to build that. The browser test was less impressed.

Putting the tests in front of publishing

GitHub Pages now publishes through the Actions workflow.

The workflow builds the site, runs the tests, then uploads the tested output for deployment. Pull requests get checked without being published. A failed build or test leaves the existing site in place.

That required switching the Pages publishing source in the repository settings as well. Adding a workflow alone wouldn’t have stopped the old publishing path.

This is probably the change I care about most. Dependency updates can come through as pull requests, get tested, and be reviewed before they affect the blog.

A few things I’d missed

We also checked keyboard navigation and contrast.

The viewport settings contained maximum-scale=1, which restricted zoom. That went. Dates and some syntax highlighting colours needed more contrast. Long code blocks could scroll sideways, but you couldn’t reach them with the keyboard.

There’s now a skip link, visible keyboard focus, and focusable code blocks. Links in the body text are underlined too.

The CI checks include these things now, though they’re still automated checks of the local site. The map and Disqus are external services, and those requests are blocked during testing.

Fewer gems

Once Actions was handling the build, we could stop using the github-pages gem bundle.

This site keeps its Hyde templates and CSS in the repository. It generates the feed and sitemap from templates too. Pagination is the only plugin it needs.

We moved to Jekyll 4 with that plugin and regenerated the lockfile. The dependency count dropped from 98 gems to 36. Comparing the generated output with the old build showed the same published file paths, so the existing post URLs survived the upgrade.

I did this with an AI agent connected to GitHub. It inspected the files, prepared pull requests, checked Actions results, and downloaded screenshots for review. There were a few rounds of fixing things the new tests found.

It was a useful job for an agent: an existing repository, fairly small changes, and a way to check the result. Having it open the pages in a browser was particularly worthwhile.

The blog hasn’t had a redesign. It’s still Markdown, Jekyll, and much the same layout I picked in 2014. I’ve just brought the build up to date and made it harder to publish something broken.

Dusting off this blog

Giving AI agents a long-term reference with Markdown and GitHub

I use a GitHub repository containing an Obsidian-style archive of Markdown files as a long-term reference for AI agents.

The idea is simple: keep useful context somewhere I can read, edit, and version, then give the agent instructions to consult it when relevant. A new conversation can pick up a project without me having to explain its entire history again.

The chat is where the work happens. The repository is where I keep the things worth carrying forward.

An archive like this can hold project notes, decisions, preferences, useful references, and explanations of why something works the way it does. Those last two are particularly useful. An agent can often discover what exists by reading code, but understanding why I chose it usually needs some additional context.

Markdown makes this easy to maintain. I can work with the files in Obsidian, a text editor, or GitHub itself. Links between notes help connect related ideas, and the files remain useful even if I change which AI tools I use.

Git adds a history of changes. If a note becomes inaccurate, I can correct it. If an agent makes an unhelpful edit, I can inspect the diff and revert it.

What the repository could look like

There is no special schema required. A small archive could start with a directory listing like this:

README.md
index.md
preferences/
    communication.md
    tools-and-workflows.md
projects/
    personal-blog/
        overview.md
        deployment.md
        decisions.md
    garden-planner/
        overview.md
        next-steps.md
reference/
    markdown-conventions.md
    useful-links.md
journal/
    2026-10-01.md
archive/
    retired-project.md

This is an example structure, rather than a requirement to organise every vault the same way.

The README.md explains what the repository is for and how to use it. The index.md links to the main topics, with a sentence describing each one. That gives an agent a useful starting point without needing to read every file.

The project folders hold current context: what I am building, how it works, what we have decided, and what remains to do. Preferences hold reusable guidance, while reference notes capture material that applies across projects. Dated journal entries can record what happened in a session; anything that changes a project’s current state should also be reflected in its project notes.

For a question about the blog’s deployment, the agent could follow index.md to projects/personal-blog/overview.md, then read deployment.md and the relevant decisions. After a change, it could update those notes and add a link from the index if it creates a new one.

The folder structure helps navigation, but the notes still need to explain themselves. A short summary, a last-reviewed date, and links to related notes make it easier to judge whether a file is relevant and current.

Giving the agent directions

Putting notes in a repository is only half the setup. The agent also needs to know where to look and when to use them.

Custom prompts or instructions can establish that behaviour. For a chat app such as ChatGPT on mobile, I can give it a standing instruction to consult the archive for questions about my projects and previous decisions, wherever the relevant GitHub connection is available.

A starting prompt might look like this:

My long-term reference notes are in the GitHub repository OWNER/REPO.

When a task depends on my previous work, preferences, or project decisions, consult that repository before answering. Start with the README or index, then read the relevant notes and follow useful links.

Retrieve the files needed for the task rather than loading the whole archive. Tell me which notes informed your answer, and flag anything that appears outdated or contradictory.

When I ask you to save a decision or update the archive, read the existing note first, make a focused change, and report what you changed. Keep confirmed decisions separate from suggestions and unresolved questions.

If you cannot access the repository, say so rather than guessing.

The prompt establishes the workflow. The connected tool provides access. Custom instructions alone do not make a repository available, so I still need to connect GitHub and check that the tools I need are supported in the app I am using.

Reading and writing through GitHub

With a GitHub plugin that supports the required operations, the archive can become part of the conversation.

I can ask the agent to read the notes for a project before suggesting a change. After we settle on an approach, I can ask it to update the relevant Markdown file or create a new note. Depending on the available tools and permissions, those changes can be committed to a branch and reviewed through a pull request.

For example:

Read the archive notes for this project and explain why we chose the current deployment approach.

Then, after making a new decision:

Update the deployment note with what we agreed, including the reason for the change and any remaining questions.

That gives the next session something concrete to retrieve. It also means I can review the saved context using the same tools I already use for code.

Keeping the archive useful

I want the repository to contain useful reference material, rather than every conversation verbatim.

A short note recording a decision, its reasoning, and the date is often more valuable than a lengthy transcript. Clear filenames and an index help an agent find the right material. Links to supporting sources make the notes easier to check.

The archive still needs maintenance. Old assumptions can become misleading, and an agent’s suggestion should not quietly turn into a recorded fact. I try to keep the distinction between “we decided this”, “we might try this”, and “this needs checking” explicit.

This approach gives me continuity that I can inspect and control. I can start a conversation on my phone, return to the work elsewhere, and direct the next agent to the same reference material.

The useful part is having a growing collection of notes that both I and the agent can consult—and a straightforward way to improve those notes as we work.

Giving AI agents a long-term reference with Markdown and GitHub