In just a few years, artificial intelligence has moved from the lab to our pockets, and then into our code editors. It writes, summarizes, translates, explains, and now it acts: it reads a project, edits files, runs tests and proposes fixes. For a developer, an architect or a CIO, the question is no longer whether to pay attention, but how to adopt it with method, efficiency and security.
This article offers a complete journey, from the general to the concrete: what AI really is, how a language model works, what sets an assistant apart from an agent, how to choose and steer a coding agent, how to write a good prompt, manage memory, configure permissions and protect data. The examples are based on Claude Code, Anthropic's coding agent, but the principles apply to most tools.
How should you read this article? The first sections lay the foundations and are accessible to everyone. The next ones describe agents and their ecosystem, then a practical guide for technical teams. The second half is a complete workshop: the LinkLite mini-project, built from A to Z with Claude Code, all the way to version 1.0.0. A glossary closes the article.
In this article
- Understanding artificial intelligence
- The major families: Machine Learning, Deep Learning, NLP, LLMs and generative AI
- AI today: a market upheaval
- AI as an everyday assistant
- From assistant to agent: AI in IT and the information system
- What is an AI agent?
- Agents, sub-agents, skills, MCP, hooks and commands
- Choosing your AI agent
- AI-driven development
- Prompt engineering: phrasing your request well
- AI memory: why and how to remember
- Useful commands: the Claude Code example
- The .claude folder: local, user, organization
- Permissions and data security
- Best practices for agent-driven coding
- Hands-on workshop: LinkLite, a mini-project from start to finish with Claude Code
- Laying the groundwork: memory, permissions and the Context7 MCP server
- The workshop prompts: from scoping to delivery
- TDD with an agent: red, green, refactor
- Designing the interface with the frontend-design skill
- Building your own agents and skills
- Saving tokens and holding the scope
- PostgreSQL and Docker: a deploy-ready application
- Git, code quality and shipping v1.0.0
- Conclusion
- Glossary
Understanding artificial intelligence
A simple definition
Artificial intelligence refers to the set of techniques that enable a machine to perform tasks we usually associate with human intelligence: recognizing an image, understanding a sentence, translating a text, making a decision, solving a problem or generating content.
Analogy: a traditional program is a cooking recipe written line by line by a human. An AI is an apprentice cook who is shown thousands of successful dishes until they work out the rules on their own.
This difference is fundamental. In traditional programming, the developer writes the rules. In machine learning, you provide examples and the machine learns the rules.

Figure 1. Traditional programming versus machine learning: in the second case, the rules are what gets produced.
A brief history
AI was not born with conversational assistants. It went through several waves of enthusiasm and disillusionment (the “AI winters”) before the current acceleration, made possible by three ingredients coming together: massive data, abundant computing power (GPUs) and new architectures of neural networks.

Figure 2. The major milestones of AI, from the first ideas to autonomous agents.
Narrow AI, general AI
Every AI in production today is so-called “narrow” or specialized AI: it excels in a specific domain (language, images, recommendations…) without consciousness or understanding of the world in the human sense. General AI (able to reason about any subject the way a human does) remains a research goal and a subject of debate. Keeping this distinction in mind avoids two symmetrical mistakes: underestimating today's tools, or crediting them with capabilities they do not have.
The major families: Machine Learning, Deep Learning, NLP, LLMs and generative AI
These terms overlap and are often wrongly used as synonyms. The best picture is a set of Russian dolls: each notion is a subset of the previous one.

Figure 3. The Russian dolls of AI: an LLM is a generative Deep Learning model applied to language (NLP).
| Notion | What it is | Concrete example |
|---|---|---|
| Machine Learning | Algorithms that learn patterns from data. | Bank fraud detection, credit scoring, sales forecasting. |
| Deep Learning | Machine Learning based on neural networks with many layers. | Medical image recognition, speech recognition. |
| NLP | Natural language processing (text and speech). | Translation, sentiment analysis, extracting information from a résumé. |
| Generative AI | Models able to produce new content: text, images, audio, code. | Generating an illustration, a summary, a draft letter. |
| LLM | Large language model trained on huge corpora of text and code. | Conversational assistants, coding agents. |
How does an LLM work?
An LLM does not “look up” an answer in a database: it predicts, word after word (more precisely, token after token), the most plausible continuation of a text. A token is a fragment of a word: “development” may be split into several tokens.
Analogy: it is your phone's autocomplete, but trained on a gigantic library and able to take entire pages of context into account. It does not recite: it composes.

Figure 4. Generation is a loop: each token produced is fed back into the context to predict the next one.
Three notions are enough to understand most of an LLM's behavior:
- The context window: the amount of text the model “sees” at any given moment (your request, the conversation history, the files it has read). Beyond that, information is no longer taken into account.
- The knowledge cutoff date: the model only knows what was in its training data. Hence the value of feeding it up-to-date documentation.
- Temperature: a setting that controls the amount of randomness. Low, responses are stable and predictable; high, they are more creative.
Hallucinations. Because it produces the most plausible text, an LLM can confidently state something false: an API function that does not exist, an invented reference, a wrong figure. The answer is not to distrust it wholesale, but to verify: tests, official documentation, review.
AI today: a market upheaval
The arrival of mainstream conversational assistants triggered adoption at an unprecedented pace. Within a few months, millions of people had built AI into their daily work, and companies had to respond on several fronts at once.
What actually changes
- Productivity: writing, summarizing, document analysis and code production are all accelerated, sometimes dramatically on repetitive tasks.
- Jobs: few jobs disappear overnight, but many are transformed. Value shifts from execution toward design, verification and decision-making.
- Sought-after skills: knowing how to express a need, evaluate a result, understand the limits of the tools and use them safely is becoming as important as technical mastery.
- Recruitment: job postings increasingly require proficiency with AI tools, and applications themselves are often screened and analyzed automatically.
- Regulation: in Europe, the AI Act is gradually regulating uses according to their level of risk, alongside the GDPR for personal data.

Figure 5. The shift in value: from execution toward expressing needs, steering and deciding.
One phrase sums up the situation well: AI will not replace professionals, but a professional who masters AI will probably replace one who ignores it.
AI as an everyday assistant
Even before talking about code, generative AI is first and foremost a versatile assistant. Used well, it saves a considerable amount of time on tasks everyone faces.
| Use | Example request | Watch out for |
|---|---|---|
| Write | “Write a friendly follow-up email about an unpaid invoice.” | Check the tone and the facts. |
| Summarize | “Summarize this 12-page meeting report into 5 action items.” | Make sure no key point is missing. |
| Translate | “Translate this paragraph into professional English.” | Watch the domain-specific vocabulary. |
| Explain | “Explain the principles of the GDPR to me as if I were a beginner.” | Cross-check with an official source. |
| Organize | “Propose a 4-week training plan.” | Adapt it to the real context. |
| Analyze | “What are the risks in this service contract?” | It does not replace a lawyer. |
Analogy: the AI assistant is a very fast, well-read colleague available at any hour, but one who has just joined the company. They know nothing about your context, your clients or your internal rules, unless you explain them.
Golden rule: never paste personal data, secrets (passwords, API keys) or confidential company information into a consumer assistant, unless your organization has approved the tool for that use.
From assistant to agent: AI in IT and the information system
In software development, AI has progressed in three stages, each giving the machine a little more autonomy.
| Chatbot | Copilot (autocomplete) | Coding agent | |
|---|---|---|---|
| Role | Answers questions | Suggests the next lines of code in the editor | Carries out a task end to end |
| Context | Whatever you paste | The open file | The whole project, the tools, the terminal |
| Actions | None | Inserts text | Reads, writes, runs, tests, fixes |
| Autonomy | None | Low | High, bounded by permissions |
| Analogy | A manual that answers back | A desk neighbor whispering hints | A junior developer you delegate to |
The coding agent fits into the entire software lifecycle: understanding existing code, design, implementation, testing, documentation, code review, migration, and even operations (log analysis, automation scripts). Within an information system (IS), it becomes a lever for modernizing legacy applications, documenting an application portfolio or industrializing repetitive tasks.

Figure 6. The agent can step in at every stage of the software lifecycle, always under the team's control.
What is an AI agent?
Definition
An AI agent is a system that combines a language model with the ability to act on its environment through tools, and that chains these actions in a loop until it reaches a goal. Where an assistant answers once, an agent observes, decides, acts, verifies, then starts again.
Analogy: the assistant is an advisor who tells you what to do. The agent is a craftsman who picks up the tools, does the work, checks the result and reports back to you.
The components of an agent

Figure 7. Anatomy of an agent: a model, a memory, an action loop and tools, governed by permissions.
The agentic loop
At the heart of an agent is a simple loop. Take a concrete request: “The sign-up test is failing, fix it.”

Figure 8. An agentic loop in action: the agent observes, acts, verifies, then reports back.
Key takeaway: an agent's value does not come from the model alone, but from its ability to check its own work (tests, compilation, linting). An agent that can run the tests fixes its own mistakes.
Agents, sub-agents, skills, MCP, hooks and commands
Around the main agent revolves an ecosystem of building blocks with clearly distinct roles. To understand them, picture a company:
| Building block | Analogy | Role | When to use it |
|---|---|---|---|
| Agent | The project manager | Understands the goal, plans, acts and coordinates. | Always: it is your main point of contact. |
| Sub-agent | An expert you hand a case file to | A specialized agent with its own context, tools and instructions; it returns a concise result. | Focused, large-scale tasks: security review, exploring a big codebase, writing tests. |
| Skill | A procedure sheet | Packaged know-how (instructions, scripts, templates) loaded only when it is useful. | Recurring procedures: generating a report, following a style guide, deploying. |
| MCP | The universal USB-C port | A standard protocol for connecting the agent to external tools and data. | Accessing Jira, GitLab, a database, documentation, a browser. |
| Hook | The automatic security check at the building entrance | A script triggered automatically at a specific moment (before or after an action). | Formatting after every edit, blocking a dangerous command. |
| Command | A desktop shortcut | A pre-recorded instruction launched with /nom. |
Repeated actions: code review, creating a component, writing a commit message. |

Figure 9. The ecosystem of a coding agent: delegation to sub-agents, know-how in skills, MCP connections and checks through hooks.
Sub-agent or skill: the key difference
The two are often confused. A sub-agent is a someone: it works in a separate context, which keeps the main conversation uncluttered, then returns a summary. A skill is a piece of know-how: it does not work on its own; it enriches the agent that loads it, at the moment the agent needs it.
Example of a sub-agent, defined in a simple Markdown file:
---
name: security-reviewer
description: Analyzes changes to detect security vulnerabilities (OWASP). Use after any change touching authentication or data.
tools: Read, Grep, Glob
---
You are an application security expert. For each change:
1. Spot injections, data leaks and missing access controls.
2. Rank each issue by severity (critical, high, medium).
3. Propose a precise fix, without editing the files yourself.
And a skill, stored in a folder alongside its resources:
---
name: release-notes
description: Writes release notes from conventional commits. Use when preparing a release.
---
1. List the commits since the last tag (git log).
2. Group them by type: feat, fix, perf, docs.
3. Write clear, user-oriented notes in English.
MCP: the universal port
The Model Context Protocol (MCP) is an open standard that lets any compatible agent connect to any compatible tool. Without it, every agent would need a dedicated integration for every tool; with it, a connector written once works everywhere, exactly like a USB-C cable that charges a phone, a laptop or headphones alike.
{
"mcpServers": {
"gitlab": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-gitlab"],
"env": { "GITLAB_PERSONAL_ACCESS_TOKEN": "${GITLAB_TOKEN}" }
}
}
}
An MCP server is third-party code that accesses your tools and data. Only install trusted servers, with tokens that have minimal permissions, and never commit a token in plain text: use environment variables.
Choosing your AI agent
Today there are several families of tools: editor-integrated assistants (IDE extensions), command-line agents that work in the terminal, and cloud agents that handle tasks in the background on a repository. The right choice depends less on hype than on objective criteria.
| Criterion | Questions to ask |
|---|---|
| Model quality | Does it reason well about complex code? Does it follow instructions? How does it perform on your languages? |
| Integration | Does it work in your IDE, your terminal, your CI/CD? Does it support MCP? |
| Context | Can it understand an entire project? Does it maintain a project memory? |
| Control | Are permissions fine-grained? Can you review every change before it is applied? |
| Privacy | Where is the data processed? Is it used for training? What contractual guarantees are there? |
| Extensibility | Sub-agents, skills, hooks, custom commands? |
| Cost | Subscription or pay-as-you-go? Is the cost predictable for the team? |
| Compliance | Is it compatible with the company’s security policy, the GDPR, the AI Act? |
Tip: evaluate two or three tools on the same real task from your project (fixing a bug, adding a tested feature) rather than on demos. Measure the result, the review time required and the quality of the tests produced.
AI-driven development
Working with an agent is not about telling it “build the app”. The teams that get the best results follow a disciplined cycle in which humans stay in charge of the decisions.

Figure 10. The AI-driven development cycle: explore, plan, implement, verify, review, commit.
A concrete example: adding a filter to a list
- Explore: “Read the orders module and explain how the list is loaded. Don’t change anything.”
- Plan: “Propose a plan to add a server-side status filter, listing the files affected and the tests you intend to write.” Then you review and adjust the plan.
- Implement: “Apply step 1 of the plan and write the tests first.”
- Verify: the agent runs the tests and keeps fixing until they are green.
- Review: you read the diff as you would a colleague’s.
- Commit: “Create a commit in Conventional Commits format.”
TDD is the agent’s best ally. Tests written first give the agent a verifiable goal: it knows objectively when it is done, and you know the result works.
Prompt engineering: phrasing your request well
Prompt engineering is the art of phrasing a request to get the best result from a model. With an agent, the stakes are even higher: a vague prompt produces vague work, across many files.
Analogy: a prompt is the brief you give a contractor. A good contractor with a bad brief delivers something well made… that doesn’t match what you need.
Anatomy of a good prompt

Figure 11. The five ingredients of a good prompt.
Bad prompt, good prompt
A vague prompt leaves the agent guessing:
Add pagination to the user list.
A good prompt frames the work:
Context: Next.js application, module src/features/users.
The user list loads every record, which slows the page down.
Goal: add server-side pagination (20 items per page).
Constraints:
- follow the existing architecture (repository > service > action);
- do not change the database schema;
- no new dependencies.
Success criteria:
- service unit tests for the first page, an intermediate page and the last page;
- all tests and the lint pass.
Start by proposing a plan, without modifying any file.
Scope: one task at a time
Scope is the number one success factor. A request that mixes a refactor, a new feature and bug fixes produces a result that is hard to review and test. Break it down: one request = one verifiable goal, ideally one commit.
Where should you write your prompt?
| Location | What for | Lifetime |
|---|---|---|
| The conversation | The request at hand | The session |
The CLAUDE.md file |
The project’s permanent rules (conventions, commands, architecture) | As long as the project exists |
| A ticket or a specification | A detailed feature, shared with the team | For the duration of the development |
| A custom command | A request repeated identically | Reusable at will |
Techniques that make a difference
- Ask for a plan first, and approve it before any change.
- Give an example of the expected result (the style of an existing test, a reference component).
- Point to the relevant files instead of letting the agent search for them.
- Invite the agent to ask questions when information is missing.
- Require verification: “run the tests and show me the result”.
- Correct early: interrupt as soon as a direction looks wrong, rather than letting the agent dig itself deeper.
AI memory: why and how to remember
An LLM has no memory between two conversations. Every new session starts from scratch. Without memory, you would have to re-explain the project conventions, the test commands, the architecture decisions… every single time.
Analogy: the context window is working memory: what you keep in mind during a meeting. Persistent memory is the logbook you reread when you arrive in the morning.

Figure 12. Two levels of memory: the persistent logbook and the session’s working memory.
What should you store in memory?
- Commands: how to run the tests, the build, the lint.
- Conventions: architecture, naming, code style, commit format.
- Decisions: technical choices and the reasons behind them (“we don’t use an ORM here because…”).
- Pitfalls: what has caused problems before and the right way to do it.
An example of a concise, effective CLAUDE.md file:
# Portfolio project: rules for the agent
## Commands
- Install: bun install
- Tests: bun run test (minimum coverage 95%)
- Checks: bun run lint && bun run typecheck
## Architecture
- Feature First: each feature in src/features/<name>/
- Mutations only through Server Actions, validated with Zod
## Rules
- Never push to main: a branch and a merge request
- Commits in English, Conventional Commits format
- Never read or modify .env files
A good memory file is short, factual and up to date. Ten precise rules beat a hundred generic lines: everything in memory takes up room in the context at every session.
Useful commands: the Claude Code example
In Claude Code, commands starting with / control the session itself. Here are the most useful ones day to day (the full list is available with /help):
| Command | What it does | When to use it |
|---|---|---|
/init |
Analyzes the project and generates a first CLAUDE.md. |
The first time you open a project. |
/memory |
Opens and edits the memory files. | To add a rule or a decision. |
/clear |
Clears the conversation context. | When switching tasks, to start fresh. |
/compact |
Summarizes the conversation to free up context. | During a long session on the same task. |
/model |
Switches models. | A more powerful model for a complex task. |
/permissions |
Views and sets permissions. | To allow a safe command or deny one. |
/agents |
Creates and manages sub-agents. | To delegate a specialty. |
/mcp |
Shows the connected MCP servers. | To check a connection to an external tool. |
/hooks |
Configures hooks. | To automate a check. |
/resume |
Resumes a previous conversation. | To continue an interrupted task. |
/config |
Opens the settings. | To customize the tool. |
/help |
Lists all available commands. | When in doubt. |
Shortcuts and custom commands
-
@followed by a path: references a specific file in the request. -
!at the start of a line: runs a shell command directly. -
Esc: interrupts the agent so you can redirect it. -
Shift + Tab: cycles through modes (including plan mode, where the agent proposes without modifying anything).
A custom command is just a Markdown file placed in .claude/commands/. The file .claude/commands/review.md becomes the /review command:
Review the uncommitted changes on the current branch.
For each modified file, check:
- compliance with the architecture and conventions in CLAUDE.md;
- the presence of tests for any new logic;
- the absence of any plain-text secret or personal data.
Return a list of comments sorted by priority.
The .claude folder: local, user, organization
An agent’s configuration is spread across several levels, from the most personal to the most collective. In Claude Code, it lives mainly in folders named .claude.
Analogy: the project’s
.claudefolder is the onboarding binder handed to every newcomer: house rules, procedures, useful contacts. The user folder is your personal toolkit, which follows you from project to project.
mon-projet/
├── CLAUDE.md # project memory, shared (versioned)
├── .mcp.json # project MCP servers (versioned, no secrets)
└── .claude/
├── settings.json # shared permissions and hooks (versioned)
├── settings.local.json # personal settings (not versioned)
├── commands/ # custom commands: /review, /release…
├── agents/ # project sub-agents
└── skills/ # project skills
~/.claude/ # user level, specific to your machine
├── CLAUDE.md # personal preferences, across all projects
├── settings.json # global personal settings
├── commands/ # your personal commands
└── agents/ # your personal sub-agents
| Level | Scope | Example content | Versioned? |
|---|---|---|---|
| Organization (managed settings) | Every workstation in the company | Global prohibitions, approved MCP servers | Managed by IT |
Shared project (.claude/, CLAUDE.md) |
The whole team on this project | Conventions, test commands, common permissions, sub-agents | Yes |
Personal project (settings.local.json) |
You, on this project | Permissions specific to your workstation | No |
User (~/.claude/) |
You, across all your projects | Preferred language, response style, favorite tools | No |
Whatever is versioned is shared with the whole team, and sometimes beyond. No secrets in
CLAUDE.md,settings.jsonor.mcp.json: use environment variables and keep.envfiles out of the repository.
Permissions and data security
An agent that can read files and run commands has real power over your machine and your code. So the question is not “should everything be allowed?” (the answer is clearly no) but “how do you allow only what is needed, at the right time?”
Analogy: you don’t hand a contractor the master key to the entire building. You give them a badge that opens the rooms they need, for the duration of their assignment.
The principle of least privilege

Figure 13. Permission control: what is denied is blocked, what is allowed goes through, everything else requires confirmation.
In practice, you allow safe, frequent actions up front (running tests, reading code), you explicitly deny anything that touches secrets or irreversible operations, and you leave everything else subject to confirmation.
{
"permissions": {
"allow": [
"Bash(bun run test:*)",
"Bash(bun run lint)",
"Bash(git status)",
"Bash(git diff:*)"
],
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Bash(git push --force:*)",
"Bash(rm -rf:*)"
]
}
}
Personal and sensitive data
- Minimize: give the agent only the data the task requires. Fictitious test data is better than a production extract.
-
Isolate secrets:
.envfiles denied for reading, tokens with limited rights, regular rotation. - Comply with the GDPR: no real personal data (customers, employees, patients) in the exchanges without a legal basis and a tool approved by the company.
- Be wary of external content: a web page, a ticket or a file can contain malicious instructions (“prompt injection”). A careful agent treats such content as data, never as orders.
- Keep a trail: review the diffs, work on branches, keep the Git history so you can roll back.
| Approach | Advantage | Risk | Verdict |
|---|---|---|---|
| Allow everything | No interruptions | Leaked secrets, destructive commands, unintended actions | ❌ Avoid |
| Confirm everything | Total control | Fatigue: you end up approving everything without reading | ⚠️ Counterproductive |
| Targeted, progressive permissions | Smooth on the safe, controlled on the sensitive | Requires a little configuration | ✅ Recommended |
Best practices for agent-driven coding
A checklist to keep at hand for every task you hand to an agent:
- The task is scoped to one clear, verifiable goal.
- The agent works on a dedicated branch, never directly on the main branch.
- A plan was proposed and approved before any changes.
- Tests describe the expected behavior, ideally written first.
- Tests, lint and build pass before the task is considered done.
- A human has reviewed the diff, as they would a colleague’s.
- No secrets or personal data appear in the code or the commits.
- The CLAUDE.md file has been updated if a new rule has emerged.
- The commit is atomic, with an explicit message.
The most common pitfalls
- Blind trust: accepting code that “looks right” without testing it.
- Giant requests: a single prompt for an entire feature, impossible to review.
-
Polluted context: an overly long session mixing several topics; better to
/clearbetween tasks. - Invisible debt: generated code that works but that nobody on the team understands.
- Skill loss: delegating without understanding. The agent should remain an accelerator, not a crutch.
The rule that sums it all up: the agent proposes, the human decides. You remain responsible for every line that goes to production.
Hands-on workshop: LinkLite, a mini-project from start to finish with Claude Code
Theory is not enough: to master a coding agent, you need to have watched it work on a real project, with its successes, its mistakes and its guardrails. This part walks through a complete mini-project built with Claude Code: from choosing the subject all the way to a tagged version 1.0.0, tested, audited and ready to deploy. Each step picks up a concept covered earlier (prompts, memory, sub-agents, skills, MCP, permissions) and puts it into practice.
What you will build: LinkLite, a URL shortener. You drop in a long address and leave with a short link (a “ticket”) and a click counter.
Full source code of the mini-project: https://github.com/abdoulayeba29/linkliteWhat you will practice: prompt engineering, project memory (
CLAUDE.md), permissions, the Context7 MCP server, the frontend-design skill, custom agents and skills, TDD, integration and end-to-end tests, PostgreSQL, Docker, Git and delivery.
Why this subject?
A good workshop subject is small but complete: it should touch every layer of a real application without overwhelming the reader. The URL shortener ticks every box:
- A clear business domain: rules that are simple to state and to test (slug format, whether a URL is allowed or not).
- A real database: uniqueness, constraints, and a click counter that must stay accurate even under concurrent access.
- An API and a user interface: enough to practice design and end-to-end testing.
- Real security stakes: a shortener can be used to disguise an internal address (SSRF) or for spam, which makes it ideal for putting an audit agent to the test.
The scope: what is in, what is out
Setting the scope before writing code is the best protection against drift, ours as well as the agent’s, which tends to “enrich” a vague request. The scope was written down in black and white, then carried into the first prompt:
| In v1.0.0 | Out of scope (deliberately) |
|---|---|
Shorten a public http/https URL |
User accounts and authentication |
Random or custom slug (/mon-lien) |
Link expiration, QR codes |
| 302 redirect and accurate click counter | Detailed statistics (country, browser…) |
| List of the 20 most recent links | Preview of the target page |
| Light/dark theme, responsive, accessible | Admin interface |
| Security: anti-SSRF, rate limiting, CSP | Cloud deployment (v1 is “ready to deploy”) |
Technical choices and the reasoning behind them
Every choice is driven by the need, not by fashion. That is also what we ask of the agent: justify its choices rather than pile up dependencies.
| Need | Choice | Why |
|---|---|---|
| Language | TypeScript (strict) on Node.js 22 | Strong typing: the agent and the compiler correct each other. |
| HTTP server | Fastify 5 | Lightweight, fast, with built-in JSON Schema input validation. |
| Database | PostgreSQL 18 (SQL) | Declarative constraints, transactions, atomic updates: exactly what a reliable counter requires. |
| Data access |
pg driver + parameterized SQL |
Three queries: an ORM would be heavier than the problem (KISS). |
| Tests | Vitest + Playwright | Fast unit/integration tests; real user journeys in a real browser. |
| User interface | Framework-free HTML/CSS/JS | One page, no front-end build: the frontend-design skill takes care of the look and feel. |
| Delivery | Docker + docker compose | The same environment everywhere, database included. |
The architecture: hexagonal, the lightweight version
The code is organized in layers whose dependencies all point toward the center. The domain (the business rules) knows nothing about Fastify or PostgreSQL: it describes what it needs as an interface (a port), which the infrastructure implements through adapters. The result: you can test all the business logic without a database, and switch databases without touching the business logic.

Lightweight hexagonal architecture: the domain sits at the center, and adapters implement its ports.
// src/domain/link.ts: the port, i.e. what the business logic needs, without saying how
export interface LinkRepository {
/** Inserts the link; returns false when the slug already exists. */
insert(link: Link): Promise<boolean>
findBySlug(slug: string): Promise<Link | null>
/** Atomically increments the click counter; returns the target or null if unknown. */
registerClick(slug: string): Promise<string | null>
listRecent(limit: number): Promise<Link[]>
}
Laying the groundwork: memory, permissions and the Context7 MCP server
Before the first line of code, we give the agent what a new colleague would receive on day one: the house rules, the authorized access and up-to-date documentation. Everything is versioned in the repository, and therefore shared by the whole team.
| File | Role | Concept from the article |
|---|---|---|
CLAUDE.md |
Commands, architecture, coding rules, Git, security | Project memory |
.claude/settings.json |
Allowed commands, forbidden reads and actions | Permissions |
.mcp.json |
Context7 MCP server | MCP |
.claude/agents/*.md |
security-auditor and test-writer sub-agents |
Sub-agents |
.claude/skills/release-notes/ |
Release know-how (notes, CHANGELOG, tag) | Skills |
.claude/commands/review.md |
/review command before merging |
Custom commands |

LinkLite's CLAUDE.md and the agent's configuration files (editor view: Monaco rendering, the VS Code engine, of the project's real files).
Permissions: allow what is safe, forbid what is dangerous
The settings.json file avoids two pitfalls: approving the same harmless command a hundred times, and letting a destructive command slip through out of fatigue. Tests, linting and reading the Git history are allowed; reading secrets, recursive deletion and pushing to main are forbidden.
{
"permissions": {
"allow": [
"Bash(npm test)", "Bash(npm run test:*)", "Bash(npm run check)",
"Bash(git status)", "Bash(git diff:*)", "Bash(git log:*)",
"mcp__context7__resolve-library-id", "mcp__context7__query-docs"
],
"deny": [
"Read(./.env)", "Read(./.env.*)", "Read(./node_modules/**)", "Read(./coverage/**)",
"Bash(git push --force:*)", "Bash(git push origin main:*)",
"Bash(rm -rf:*)", "Bash(docker compose down -v:*)"
]
}
}
What actually happened during the workshop: the agent wanted to delete old screenshots with
rm -f docs/screenshots/*. Claude Code’s guardrail blocked the wildcard deletion, and the agent cleanly worked around the need (the files were being rewritten anyway). That is exactly what permissions are for: making the costly mistake impossible.
Context7: up-to-date documentation, right in the context
A language model has a knowledge cutoff: it knows libraries as they were when it was trained. But APIs move fast: a renamed option, a changed default, a major version that breaks everything. The classic result: plausible code… that does not compile, or worse, compiles but misbehaves.
Context7 is an MCP server that solves this problem: it indexes the official documentation and examples of thousands of libraries, version by version, and serves them to the agent on demand. Instead of “remembering”, the agent looks it up.
Installing Context7
Two equivalent approaches, at project level (recommended, since it is shared with the team):
# Option 1: from the command line, project scope (writes .mcp.json)
claude mcp add --transport http --scope project context7 https://mcp.context7.com/mcp
# Check the connection from a Claude Code session
/mcp
// Option 2: directly in .mcp.json, at the repository root
{
"mcpServers": {
"context7": { "type": "http", "url": "https://mcp.context7.com/mcp" }
}
}
Context7 exposes two tools: resolve-library-id (find a library’s identifier, for example /fastify/fastify) and query-docs (ask a targeted question about that library). A free API key raises the quotas, but is not required to get started.

The Context7 cycle: the agent resolves the library identifier, queries the documentation, then writes the code.
Using it day to day
-
The habit: add “use context7” at the end of any prompt that involves a library. You can also put it in
CLAUDE.mdso it happens every time. -
Precise questions: “
webServeroptions inplaywright.config” uses far fewer tokens than “Playwright documentation”. -
Pre-approved tools: both Context7 tools are in the
allowlist ofsettings.json, so they do not ask for confirmation.
What Context7 actually changed in LinkLite
| Library | Question asked | What the up-to-date docs prevented |
|---|---|---|
| Vitest 4 | v8 coverage and threshold configuration | A configuration written for the API of the installed version (v4), not an earlier one. |
| Playwright |
webServer, mobile projects, dark theme |
A test server started by hand: Playwright starts the application and waits for it on its own. |
| @fastify/rate-limit | Per-route limit, handling the 429 response | A 429 turned into a 500 by the global error handler: the docs flagged it, and a test locks it in. |
| Fastify 5 |
trustProxy behind a reverse proxy |
The most telling case: trust based on a “hop count” no longer works (it now fails closed to prevent IP spoofing). Code written “from memory” would have been wrong. |
Context7 does not replace tests. Documentation says what should work; only tests prove what does. In LinkLite, every piece of information that came from Context7 was locked in by a test.
The workshop prompts: from scoping to delivery
You do not steer an entire project with one giant prompt, but with a series of short, verifiable requests, each ending with an acceptance criterion. They all follow the same template:
CONTEXT : where things stand, files involved (@src/domain/...)
GOAL : a single observable outcome
CONSTRAINTS : CLAUDE.md rules, what NOT to do
VERIFICATION: the command that proves it is done (tests, lint, screenshot)
FORMAT : plan first / diff / table…
The complete sequence
| Step | Prompt (summary) | Mode |
|---|---|---|
| 1. Scoping | “Here is the subject and the scope. Propose the architecture and the list of files, without writing code.” | Plan |
| 2. Foundation | “Initialize the project: strict TypeScript, Vitest, ESLint, Prettier, CLAUDE.md and settings.json.” | Edit |
| 3. Domain (red) | “With the test-writer agent, write the URL and slug validation tests. They must fail.” | Sub-agent |
| 4. Domain (green) | “Implement the minimum to make these tests pass, without modifying them.” | Edit |
| 5. Use cases | “Create LinkService behind the LinkRepository port; in-memory adapter for the tests.” | Edit |
| 6. API + database | “Add the Fastify API and the PostgreSQL adapter. use context7” | Edit + MCP |
| 7. User interface | “/frontend-design A single page for LinkLite… (see below)” | Skill |
| 8. End to end | “Write the desktop and mobile Playwright journeys, with screenshots. use context7” | Edit + MCP |
| 9. Containers | “Multi-stage non-root Dockerfile, .dockerignore, compose with PostgreSQL.” | Edit |
| 10. Audit | “Run the security-auditor agent on the whole application.” | Sub-agent |
| 11. Fixes | “Fix every High and Medium finding using TDD, then rerun the audit.” | Edit + sub-agent |
| 12. Delivery | “Prepare v1.0.0 with the release-notes skill.” | Skill |
Example: the step 4 prompt
Context: the tests in tests/unit/target-url.test.ts are failing (red step).
Goal: implement parseTargetUrl in src/domain/target-url.ts.
Constraints:
- do NOT modify the tests;
- no external dependencies: only node:net and the URL API;
- follow the CLAUDE.md architecture (the domain imports nothing else).
Verification: npm test must pass, then npm run check.
Format: show me the diff, then the test output.
Why “do not modify the tests”? Faced with a red test, an agent may be tempted to adjust the test rather than the code. Saying it explicitly closes that door: the test is the specification.
TDD with an agent: red, green, refactor
Test-driven development is an agent’s best ally: it turns a vague intention into an objective criterion. The agent no longer has to guess what “correct” means: it reads it in the tests, and it knows when it is done.

The TDD cycle, applied to every LinkLite business rule.
The LinkLite test pyramid
| Level | Tool | What is tested | Volume |
|---|---|---|---|
| Unit | Vitest | Domain and use cases, with no database or network (in-memory adapter) | ≈ 60 tests, a few milliseconds |
| Integration | Vitest + real PostgreSQL | SQL, constraints, 20 concurrent clicks, SQL injection, migrations; API via app.inject()
|
≈ 25 tests |
| End to end | Playwright | Real user journeys in Chrome, desktop and mobile | 10 journeys |

The target URL tests, including the bypasses found by the audit, and their actual run (editor view: Monaco rendering of the real files and output).
Coverage as a guardrail
Coverage does not prove the code is correct, but it reveals what has never been executed. The threshold is blocking: below 90%, the command fails, both locally and in continuous integration.
// vitest.config.ts
coverage: {
provider: 'v8',
include: ['src'],
exclude: ['src/server.ts', 'src/infrastructure/migrate-cli.ts'], // entry points
thresholds: { lines: 90, functions: 90, branches: 90, statements: 90 },
},

The anti-SSRF filter (net.BlockList) and the final coverage report: 89 tests, 98.87% of statements covered.
A real bug, caught thanks to the process
First time opening the interface in the browser: the page loads… with no styling at all. The diagnosis is quick: the GET /:slug route, meant to redirect a short link, was also catching /styles.css and /app.js, mistaking them for unknown slugs.

Actual Chrome screenshot: the stylesheet was being intercepted by the /:slug route.
The fix followed the TDD cycle, at the scale of a single bug:
-
Red: an integration test requires
/assets/styles.cssto respond200withtext/css: it fails (302). -
Green: static files are served under the
/assets/prefix, which is already a slug reserved by the domain: no collision possible. - Guardrail: the test stays in the suite: this bug cannot come back without breaking continuous integration.
it.each([
['/assets/styles.css', /text\/css/],
['/assets/app.js', /javascript/],
])('sert la ressource statique %s sans la confondre avec un slug', async (url, type) => {
const res = await app.inject({ method: 'GET', url })
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toMatch(type)
})
Designing the interface with the frontend-design skill
Left to its own devices, a model produces “average” interfaces: the same font, the same purple gradient, the same rounded cards. That makes sense: it reproduces the average of what it has seen. The frontend-design skill, published by Anthropic, corrects this bias: it forces the agent to commit to a bold art direction before writing a single line of CSS.
What the skill contains
- A mandatory thinking phase: who the interface is for, what tone (brutalist, editorial, retro, luxurious…), what technical constraint, and above all which detail will make it memorable.
- Precise guidelines: distinctive typography (no Inter or Arial), a dominant palette with a sharp accent, targeted animations, texture and depth rather than flat fills.
- Explicit don'ts: generic “AI-generated” aesthetics.
Installing and invoking it
A skill is just a folder containing a SKILL.md. This one is available in Anthropic's public skills repository; you copy it either at user level (all projects) or at project level (shared with the team):
~/.claude/skills/frontend-design/SKILL.md # user scope
.claude/skills/frontend-design/SKILL.md # project scope (versioned)
Claude Code then loads only its description at startup; the full content enters the context only when the skill is used: automatically if the request is about an interface, or explicitly with /frontend-design.
The prompt used for LinkLite
/frontend-design Single-page interface for LinkLite, a URL shortener:
form (URL + optional custom slug), result with a copy button,
list of the 20 most recent links with a click counter, a message when a link
is not found (?missing=slug). Vanilla HTML/CSS/JS with no framework, served by
Fastify, strict CSP (no inline script, Google Fonts styles allowed),
accessible, responsive, light and dark mode.
Note that the prompt describes the need and the constraints, not the style: it is the skill that leads the agent to propose a direction. It settled on the concept of a “link check-room”: you drop off a URL like a piece of luggage and leave with a ticket: kraft paper, ink, signal orange, perforated tickets and a click counter applied like a rubber stamp.
| Design decision | Implementation |
|---|---|
| Typefaces with character | Bricolage Grotesque for headings, IBM Plex Mono for body text (a “printed ticket” register). |
| Texture | Lined kraft background, SVG paper grain, hard offset shadows. |
| Motion | Staggered reveal of the ledger; the new ticket “drops in”; prefers-reduced-motion is respected. |
| Dark theme | CSS variables redefined under prefers-color-scheme: dark. |
| Security | No innerHTML: all data goes through textContent (no XSS), and an external script that complies with the CSP. |

Real Chrome screenshot of the home page: the “counter” form and the drop-off ledger.

Real Chrome screenshot: the ticket generated after creating the /claude-code link.

Real Chrome screenshot: the cloud metadata address 169.254.169.254 is rejected.

Screenshots generated by the Playwright tests: mobile (Pixel 7) in light theme, desktop in dark theme.
End-to-end tests also produce the demo
The Playwright journeys run against the real stack (Fastify + PostgreSQL), on desktop and mobile. With SCREENSHOTS=1, they also record the documentation screenshots along the way: the demo can never fall behind the code.
test('refuse une adresse interne (protection SSRF)', async ({ page }) => {
await page.goto('/')
await page.getByLabel('URL à raccourcir').fill('http://169.254.169.254/latest/meta-data')
await page.getByRole('button', { name: 'Raccourcir' }).click()
await expect(page.getByRole('alert')).toContainText('adresses internes sont interdites')
await expect(page.locator('#result')).toBeHidden()
await capture(page, 'ssrf') // only captures when SCREENSHOTS=1
})

The Playwright journeys and their real run: 10 green tests on desktop and mobile.
A failing test does not always point to the code. The “slug already taken” journey failed at first: it filled in the form a second time before the first response had reset it. The fix belonged in the test (wait for the first ticket), not in the application. Reading the error before “fixing” anything remains a human reflex you should demand from the agent.
Building your own agents and skills
The tools you get out of the box are a starting point. Real productivity comes from agents and skills tailored to your project: they capture know-how once and for all, and every team member benefits.
A security audit sub-agent
A sub-agent is a Markdown file with a YAML header: a name, a description (which says when to use it), the allowed tools and a model. This one is deliberately read-only: an auditor analyzes, it does not fix.
---
name: security-auditor
description: Audite le code de LinkLite pour détecter des failles de sécurité
(OWASP Top 10, SSRF, injection SQL, open redirect, secrets). À utiliser après
toute modification des routes HTTP, de la validation d'URL, de l'accès base de
données ou de la configuration Docker. Ne modifie jamais les fichiers.
tools: Read, Grep, Glob
model: sonnet
---
Tu es un auditeur de sécurité applicative senior. Tu analyses, tu ne corriges pas.
## Méthode
1. Liste les points d'entrée : routes Fastify, variables d'environnement, Dockerfile, compose.
2. Pour chaque point d'entrée, vérifie : injection SQL, validation des entrées,
open redirect / SSRF, XSS, secrets, conteneur.
3. Classe chaque constat : Critique, Élevé, Moyen, Faible.
## Format de réponse
Un tableau | Gravité | Fichier:ligne | Problème | Correctif proposé |, trié par gravité.

The security-auditor sub-agent: read-only tools, a cheaper model, and an enforced method and output format.
The audit in real conditions
Run against the “finished” application (62 green tests, 97% coverage), the auditor still found plenty to do. It even verified its hypotheses by running the code instead of speculating:
| Severity | Finding | Fix |
|---|---|---|
| High | Filter bypass via IPv6: http://[::ffff:127.0.0.1]/ (IPv4-“mapped”) and NAT64 were accepted. |
Filter rewritten with net.BlockList and the full list of non-public ranges. |
| High | A trailing dot was enough: http://localhost./ slipped past the checks. |
Hostname normalized before validation. |
| Medium | Reserved ranges accepted (CGNAT, including Alibaba Cloud's metadata address). | Ranges added and tested. |
| Medium | Hostnames without a domain (http://db:5432/): an internal Docker service. |
Single-label hostnames rejected. |
| Medium | No rate limiting: link spam and inflated counters. |
@fastify/rate-limit: 120 req/min, 10 creations/min per IP. |
| Low | No request size limit, incomplete CSP, connection pool without timeouts. | Body capped at 4 KB, object-src 'none', PostgreSQL timeouts. |
Each finding was first turned into a red test (twelve at once for the bypasses), then fixed. A second pass by the auditor confirmed the fixes and flagged a few residual low-severity cases, which were fixed in turn. It also caught that a newly added range was mistakenly blocking… every IPv4 address: a test that “accepts 8.8.8.8” exposed it immediately.

The audit → tests → fixes → re-audit loop, until only low, documented risks remain.
A release skill, with references and a script
A skill goes further than a reusable prompt: it is a folder that can bundle reference documents and scripts. The agent reads the references only when it needs them (this is “progressive disclosure”) and runs the scripts without copying them into its context.
.claude/skills/release-notes/
├── SKILL.md # when to use it + the steps
├── references/
│ ├── commit-types.md # commit type → release notes section
│ └── release-template.md # exact release notes template
└── scripts/
└── collect-commits.sh # commits since the last tag, grouped by type

The release-notes skill: the steps in SKILL.md, and the real output of its commit collection script.
A command for code review
Finally, a custom /review command (file .claude/commands/review.md) reviews the current branch against main: architecture compliance, SOLID and DRY principles, presence of tests, no secrets. It ends with either “Ready to merge” or the list of blocking issues.
Saving tokens and holding the scope
Every file read, every command output, every documentation page takes up space in the context window. A cluttered context costs more, slows the agent down and degrades the quality of its answers: it “forgets” the instructions given at the start. Saving tokens therefore also means working better.
| Technique | How LinkLite applies it | Effect |
|---|---|---|
| Delegate to sub-agents | Both audits ran in a sub-agent (≈ 57,000 then 69,000 working tokens); only the final table came back. | The main context stays focused on the code. |
| The right model for the task |
model: sonnet for the auditor and the test writer. |
Cheaper for well-scoped work. |
| Block useless reads |
node_modules, coverage, dist under deny. |
No thousands of lines read by mistake. |
| On-demand skills | Only the description is loaded; the content, when it is needed. | Rich know-how with no standing cost. |
| Targeted Context7 queries | One precise question per library. | A few excerpts instead of an entire documentation set. |
| Filtered output | ` | tail -5, grep` on errors. |
A concise CLAUDE.md
|
One page: commands, architecture, rules. | Loaded in every session: every line counts. |
/clear and /compact
|
Between two independent steps. | Start again from a clean context. |
Scope is the first saving. All the “extra” ideas (analytics, QR codes, accounts) went onto the out-of-scope list rather than into the code. A feature nobody asked for costs tokens to write, to test, to audit… and to maintain.
PostgreSQL and Docker: a deploy-ready application
Why PostgreSQL, and hand-written SQL
The critical rules live in the database itself: even if a bug slipped past application-level validation, PostgreSQL would reject invalid data. Migrations are plain, versioned SQL files, applied inside a transaction and tracked in a schema_migrations table.
-- db/migrations/001_create_links.sql
CREATE TABLE IF NOT EXISTS links (
slug VARCHAR(32) PRIMARY KEY CHECK (slug ~ '^[a-z0-9][a-z0-9-]{2,31}$'),
target_url VARCHAR(2048) NOT NULL,
clicks INTEGER NOT NULL DEFAULT 0 CHECK (clicks >= 0),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS links_created_at_idx ON links (created_at DESC);
The click counter shows why SQL matters: reading the value, adding 1 and writing it back would lose clicks under concurrent access. A single atomic statement solves the problem, and an integration test fires 20 clicks in parallel to prove it.
async registerClick(slug: string): Promise<string | null> {
// Single atomic statement: concurrent clicks are never lost
const { rows } = await this.pool.query<{ target_url: string }>(
'UPDATE links SET clicks = clicks + 1 WHERE slug = $1 RETURNING target_url',
[slug] // parameter: never concatenated, so no SQL injection
)
return rows[0]?.target_url ?? null
}
The Dockerfile: multi-stage, lightweight, non-root
# 1. Build: development dependencies + TypeScript compilation
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
# 2. Production dependencies only
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
# 3. Final image: no build tools, non-root user
FROM node:22-alpine AS runtime
ENV NODE_ENV=production PORT=3000
WORKDIR /app
COPY --from=deps --chown=node:node /app/node_modules ./node_modules
COPY --from=build --chown=node:node /app/dist ./dist
COPY --chown=node:node package.json ./
COPY --chown=node:node public ./public
COPY --chown=node:node db ./db
USER node
EXPOSE 3000
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- "http://127.0.0.1:${PORT}/health" > /dev/null || exit 1
CMD ["node", "dist/server.js"]
The .dockerignore matters as much as the Dockerfile: it keeps secrets (.env), Git history, tests and the agent's configuration out of the image.
docker compose: the database, the migrations, then the application

Startup order is guaranteed by conditions: the database must be healthy, then the migrations must succeed.

The docker-compose.yml: healthcheck, migration job, read-only application container and ports bound to 127.0.0.1.
cp .env.example .env # pick a real POSTGRES_PASSWORD
docker compose up --build # PostgreSQL 18 → migrations → application
open http://localhost:3000
Transparency: Docker was not installed on the workshop machine, so the image could not be built locally. To avoid shipping an unverified file, two safeguards were used: the exact layout of the image (compiled code + production dependencies only) was reproduced and tested outside a container (migration, redirect, static assets), and the CI builds the image and starts the whole stack on every change. An honest agent reports what it could not verify; insist on it.
Git, code quality and shipping v1.0.0
One branch per intent, main always stable
No commit was made directly on main after initialization: each step lives on its own branch, and main only moves forward when everything is green. The history tells the story of the project, in order:

LinkLite's history: short-lived branches, conventional commits, then a tagged release on main.
Messages follow Conventional Commits (feat, fix, test, build, docs, chore), in English. This is not just a cosmetic convention: it is what lets the release-notes skill generate release notes automatically. Here, with no remote repository, main was only ever advanced by fast-forward (git merge --ff-only), which keeps the history linear; in a team, this step goes through a reviewed merge request and a protected main branch.
Clean Code, SOLID and DRY: concrete examples
| Principle | Where to see it in LinkLite |
|---|---|
| S: single responsibility |
parseTargetUrl validates, LinkService orchestrates, PostgresLinkRepository stores, and the routes translate HTTP ↔ domain. |
| O: open/closed | A new storage backend (Redis…) = a new adapter, with no change to the use case. |
| L: substitution | The in-memory and PostgreSQL adapters are interchangeable: the same behavior suite applies to both. |
| I: focused interfaces | The LinkRepository port contains only the four operations the domain needs. |
| D: dependency inversion |
LinkService receives its repository, its clock and its slug generator: everything is injectable, and therefore testable. |
| DRY | A single positiveInteger function validates every numeric configuration variable. |
| KISS | No ORM for three queries, no front-end framework for a single page. |
| Fail fast |
loadConfig refuses to start without DATABASE_URL or with an invalid value. |
Continuous integration
The pipeline replays everything that was done locally, on a fresh machine: types, lint, formatting, tests with coverage against a PostgreSQL service, dependency audit, Playwright journeys, then building the Docker image and starting the full stack. A branch with a red pipeline does not get merged.
Shipping, with the release-notes skill
The prompt “Prepare v1.0.0 with the release-notes skill” ran through the skill's steps:
- Collected the commits since the start of the project, grouped by type (the skill's script).
- Wrote user-oriented notes following the reference template (the effect, not the implementation).
- Added the
[1.0.0]section at the top ofCHANGELOG.mdand bumpedpackage.jsonto 1.0.0. - Blocking checks:
npm run check, coverage (98.9%) and 10 green end-to-end journeys. - Commit
chore(release): v1.0.0on arelease/v1.0.0branch. - Advanced
mainby fast-forward, then placed an annotatedv1.0.0tag onmain.
$ git log --oneline --decorate -3
93a1936 (HEAD -> main, tag: v1.0.0, release/v1.0.0) chore(release): v1.0.0
d369e61 (fix/security) fix(security): address the low-severity findings of the re-audit
e735e9b docs: add the MIT license
The results in numbers
| Metric | Result |
|---|---|
| Unit and integration tests | 89, all green |
| Coverage | 98.9% of statements, 98.8% of branches (threshold: 90%) |
| End-to-end journeys | 10 (desktop + mobile) |
| Security audit | 2 High and 4 Medium fixed, residual low risks documented |
| Bugs caught before release | Static assets intercepted, SSRF bypasses, 429 turned into 500, IPv4 regression |
| Release |
v1.0.0 tag, CHANGELOG, multi-stage Docker image, CI pipeline |
The key takeaway from the workshop: the agent wrote most of the code, but the quality came from the framework around it: a written scope, a project memory, tests first, up-to-date documentation via Context7, a design guided by a skill, an independent auditor and a tooled release process.
Conclusion
Generative AI has crossed a threshold: having learned to answer, it can now act. For development teams and IT departments, coding agents are a major opportunity, provided they are treated for what they are: very fast collaborators who need a clear framework (memory, conventions), a verifiable goal (tests), guardrails (permissions, data security) and human review.
The LinkLite workshop showed it in practice: a well-framed agent delivers a tested, audited and containerized application in a few steps, and the mistakes it makes are caught by the tests, the auditor and the permissions rather than by users. The best way to start is simple: pick a real but limited task, run /init, enrich the CLAUDE.md, configure a few cautious permissions, then follow the cycle of explore, plan, implement, verify, review. It is through methodical practice that AI becomes a true skills multiplier.
Glossary
| Term | Definition |
|---|---|
| AI agent | A system that combines a language model, tools and an action loop to achieve a goal. |
| Context7 | An MCP server that provides the agent with up-to-date, versioned library documentation. |
| Context (window) | The amount of text the model can take into account at any given moment. |
| Hallucination | False information confidently produced by a model. |
| Hook | A script triggered automatically before or after an agent action. |
| LLM | Large Language Model. |
| MCP | Model Context Protocol: an open standard for connecting an agent to tools and data. |
| Port / adapter | Hexagonal architecture: an interface defined by the domain (port) and an interchangeable technical implementation (adapter). |
| Prompt | An instruction or request addressed to the model. |
| Skill | Packaged know-how (instructions, scripts) loaded by the agent when it is useful. |
| Sub-agent | A specialized agent, with its own context, to which the main agent delegates a task. |
| SSRF | Server-Side Request Forgery: abusing a server to reach internal addresses. |
| TDD | Test-Driven Development: writing the failing test before the code that makes it pass. |
| Token | A fragment of text, the basic unit processed by a language model. |
Originally published at abdoulayeba.online.
Top comments (1)
The audit section is the part I would copy first. One residual class worth naming: if you ever add a server-side fetch (link preview, availability check), creation-time validation stops being enough. A hostname can resolve to a public IP when parseTargetUrl checks it and to an internal one when the fetch happens. The pattern that held up for me is resolving at fetch time, validating that IP, and connecting to the address you validated rather than the name. Your 8.8.8.8 test made me smile. I hit the same thing when a blocklist update silently rejected most of IPv4, and the positive control caught it before staging did.