
Every programmer has faced them: upgrading to the latest version of a framework, updating a library that introduces breaking changes across 20 different projects, migrating 400 data access objects from one database to another. These tasks are tedious and unrewarding.
We would rather spend our time thinking about complex problems or finding that elusive bug that plagues our users.
But we get fatigued by applying the same checklist to the 25th instance of a problem.
Why LLM agents are well-suited for grunt work
We call this grunt work: repetitive, pattern-based, necessary but not creative, time-consuming tasks prone to human errors.
These tasks are especially well suited for LLM agents. We provide the pattern or to-do list once, and the agent applies it to all our projects. We just have to verify that it’s done correctly.
LLM agents don’t get bored. They don’t refuse to apply the same pattern 100 times, and they do it with good quality, though not perfectly.
At Willhaben, our LLM agent of choice for most developers is Claude Code. While we make many other models available to our developers to choose from, most of them found Anthropic’s models in combination with the Claude Code CLI most useful. Moving forward from here, my examples will be referencing Claude Code, but the principles can be applied to most other agents.
Using skills with Claude Code is an effective way to get these jobs done.
Claude Code Skills: A primer
If you don’t already know, a skill for Claude Code is just a markdown file with a special header. You have to place it in a certain directory. Once you start Claude, it will automatically detect all skills in these directories, and they are available to Claude and ready to use.
Since Claude Code version 2.1.3 there was an important change regarding skills (CHANGELOG.md). Previously, skills and commands were two distinct mechanisms to do similar things. While skills were invoked automatically by Claude based on context, commands were invoked directly by the user by typing / and then the command name.
Since version 2.1.3, skills and commands were merged into one. That means a skill can also be invoked by a /command.
Here’s a simple example of how a skill looks:
.claude/skills/awesome-skill/SKILL.md
---
name: awesome-skill
description: one sentence description
---
# markdown freetext
to describe the skill
Skills can be invoked in two ways:
- Automatic — Claude loads when relevant to task (based on description)
- Manual — type /awesome-skill
Examples from Willhaben
In this section, I will show a few examples of skills that we use at Willhaben.
Check if MR is Reviewed
This example illustrates the techniques mentioned above. Claude checks whether colleagues have reviewed my merge requests and whether there are any open comments before I can merge.
This skill uses the glab CLI to retrieve data from the current merge request associated with the branch in the current directory.
This is achieved by using the bang (!) command at the bottom—it is executed before Claude sees the skill body and the output of the shell command following the ! is inserted directly there. If we include the allowed-tools header at the top, the user doesn’t have to manually allow the invocation of the shell command. Using JSON as an output format proved much more stable than just letting Claude parse the text.
---
name: review-done-yet
description: checks if for the current branch, the GitLab review is approved yet or if there are open comments or reviews
allowed-tools: Bash(glab:*)
---
# Analyze merge request status
Looking at the output at the bottom, answer this:
Is the MR ready to merge (done yet)?
Are there approvals missing?
Are there open comments or notes?
Use this template to respond and only output this text, no other summary:
<answer if the MR is ready to merge with a funny one-liner in the style of a British butler>
Approvals: assigned: <x> approved: <x>
Build: <pass|failed based on Pipeline status, mark green/red terminal colors>
Open Comments:
- <line of code referenced>:<author>:<summary>
# Output of `glab mr view`
!`glab mr view --comments -F json`
Example invocation:
❯ /review-done-yet
⏺ I regret to inform you that certain matters
require attending to before we may proceed, sir.
Approvals: assigned: 4 approved 1
Build: ✅ pass
Open Comments:
- General MR description: Bernhard V.: Add description explaining why the split was done
- SearchDao.java:28: Bernhard V.: Do not use JdbcSupport for new classes, use JDBC Template instead
Of course, after invoking the skill, Claude can continue to work, for example, on the comments and notes in the merge request.
Spring Boot 4 upgrade
In November last year, Spring Boot 4 was released. So naturally, we have to upgrade all of our projects — and there are plenty of them. Spring Boot 4 introduced a few breaking changes when upgrading from version 3 to 4, but we can just give Claude an example for each of those changes in a skill file and it will mostly fix it for us. Here, I will just give a very brief excerpt of the skill that illustrates one of those changes.
Skill excerpt:
---
name: spring-boot-4-upgrade
description: Upgrades Java projects from Spring Boot 3.x to Spring Boot 4.x. Use when the user asks to upgrade Spring Boot, migrate to Spring Boot 4, or update Spring dependencies to version 4.
---
...
Issue: SecurityFilterChain builder pattern changed
Fix: Update to lambda-based configuration:
Old style (Spring Boot 3):
return http.authorizeHttpRequests()
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.userInfoEndpoint()
.userAuthoritiesMapper(this::mapAuthorities)
.and()
.and()
.build();
New style (Spring Boot 4):
return http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated())
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(this::mapAuthorities)))
.build();
...
Spring Boot 4 introduced a new style of writing rules for authorizing requests. Rewriting those rules from one format to another can be quite tedious. In this case, all we needed to do was provide Claude with this example, and this was sufficient to get most of the rules rewritten automatically. In combination with integration tests, you can verify that the behavior didn’t change.
What’s still left for humans
Software engineers should obviously check Claude’s work. In my experience, using these kinds of skills gets you 80 to 90% to your goal in much less time than it would take if you typed the code yourself.
This frees up cognitive load to think about problems that AI cannot solve, or where it’s not good at — at least not yet. One example for this is that while Claude can generate skeletons for integration and unit tests very well, the test cases themselves it creates are often not covering all the functionality of the class or module it’s testing. But using a skill to generate all the boilerplate, and just having to specify what exactly it should test and provide domain knowledge about those cases, is a much more rewarding task than setting up the Spring context for tests for the 100th time.
Takeaway: Use Skills + CLI tools instead of MCP servers
I want to point out that it is often much more efficient to use a skill in combination with command line tools, like we showed before with the merge request skill, instead of using an MCP server or tools that are provided by the MCP server.
Adding an MCP server often fills up a substantial portion of the context of the model, even if you don’t use any of the tools exposed from the server at all.
Skills, on the other hand, are very lightweight on context.
Wrapping Up
I hope I could show you one trick or two about how to use skills with Claude code and give you some insights of how we use them at Willhaben.
AI vs Grunt Work: Reducing Repetitive Programming Tasks with Claude Code Skills was originally published in willhaben Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.