MoreRSS

site iconHackerNoonModify

We are an open and international community of 45,000+ contributing writers publishing stories and expertise for 4+ million curious and insightful monthly readers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of HackerNoon

How DPRK Hackers Claimed 51% of 2025's Crypto Losses

2026-03-14 00:11:33

Crypto hit a strange paradox in 2025. On one hand, institutional roots deepened and user bases hit historic highs, with Binance crossing the 300 million registered users mark. On the other hand, the industry faced the industrialization of cybercrime. Web3 suffered roughly $4 billion in total losses last year, and more than half, over $2 billion, went directly to actors linked to North Korea.

We are seeing a move away from simple code exploits toward something far more dangerous. State-sponsored groups are now hitting centralized infrastructure with a level of precision that mirrors military operations. This is a distinct turning point. We have moved past the era of random, opportunistic smash-and-grabs. Instead, we are now witnessing organized financial extraction operating at a global industrial level.

North Korean Hackers Account for Most of 2025's Web3 Losses

The numbers from 2025 reveal a disturbing efficiency in state-backed cybercrime. North Korea-linked groups stole a minimum of $2.02 billion, according to findings from Hacken's 2025 Security Report and the Chainalysis 2026 Crypto Crime Report. Despite executing fewer individual attacks, the value of funds stolen by these actors rose 51% year-over-year. With one nation-state driving 52% of global Web3 losses, cyber theft has evolved into a critical revenue engine for the regime.

The tactics employed by groups like Lazarus have shifted away from the scattershot approach of previous years. In 2025, these actors engaged in "Big Game Hunting," moving their focus from decentralized finance bridges to centralized infrastructure where larger pools of liquidity reside.

The compromise of Bybit stands as the year's defining outlier event, accounting for nearly $1.5 billion of the total losses. This single breach skewed the year's security data significantly, pushing the ratio between the largest hack and the median hack to over 1,000 times, according to Chainalysis.

These breaches rarely start with code exploits; they start with people. Brute-force hacking is out and social engineering is in. North Korean operatives have pivoted to using fake recruiter profiles and IT worker scams to physically or digitally embed themselves within crypto firms. Sometimes they get hired; other times, they trick executives into downloading malware.

By compromising the people who hold the keys to internal infrastructure, they render traditional firewalls useless. Human manipulation has become their most lethal tool.

The 'Chinese Laundromat'

Stealing billions in digital assets is only the first phase of the operation; converting those assets into usable fiat currency without detection is the second. North Korean actors have developed a highly complex laundering pipeline to address this challenge. TRM Labs identifies this network as the Chinese laundromat, a system where DPRK actors outsource the liquidation process to a web of over-the-counter (OTC) brokers and underground banking channels.

These brokers often settle transactions in CNY or physical goods, effectively bypassing blockchain tracing mechanisms that track on-chain movement. The laundering process is methodical and patient.

Once funds are taken, they enter a complex 45-day laundering cycle. Chainalysis data maps the route these assets take, a complex journey through mixers, cross-chain bridges, and swap protocols, before they eventually exit the crypto ecosystem. It functions less like a getaway car and more like a sophisticated and transnational pipeline. The goal is to wash illicit funds so thoroughly through the global economy that tracing them back to the source becomes nearly impossible.

Stepping up Crypto Security

Combating this level of threat requires a fundamental pivot in how the industry approaches security. The focus must shift from solely auditing smart contract code to auditing operational security and human processes. As attacks target personnel and internal infrastructure, defense mechanisms must evolve to protect the human layer.

Major players are responding to these threats at scale. With over 300 million users, Binance provides a clear example of how to harden defenses against state actors. According to its 2025 Year in Review, the exchange stopped $6.69 billion in potential fraud, shielding more than 5 million users. Much of this success came from internal Red Team simulations that trained staff to spot social engineering attempts. The impact was immediate as employee phishing failure rates fell from 3.2% in 2024 to only 0.4% in 2025.

Noah Perlman, Chief Compliance Officer at Binance, emphasizes that security improvements are quantifiable even amidst massive growth.

"Analysis of independent industry data shows a steep reduction in our direct illicit exposure between early 2023 and mid-2025," Perlman states, noting that this decline occurred despite the platform remaining the primary venue for global liquidity. He adds that this reduction was sustained "even as Binance handled volumes comparable to the next six largest exchanges combined."

The data confirms that trust by design goes beyond a simple marketing buzzword—it's an operational necessity. Platforms can only effectively narrow the state-sponsored attack surface by running real-time threat detection in tandem with strict compliance protocols.

Industry Safeguards Against Sophisticated Attacks

A review of the 2025 figures shows a diverging pattern. While state-sponsored actors are stealing larger sums, the industry is simultaneously closing the gap in detection and prevention. With North Korean actors driving such massive losses, the market has learned that operational resilience requires more than just auditing code.

As cryptocurrency continues its integration into the global financial system—evidenced by the surge in institutional adoption—security ceases to be merely a technical feature. It becomes the foundational requirement for the future of finance.

\

:::tip This story was distributed as a release by Jon Stojan under HackerNoon’s Business Blogging Program.

:::

\

The HackerNoon Newsletter: Why Physical AI Must Be Superhuman (3/13/2026)

2026-03-14 00:03:16

How are you, hacker?


🪐 What’s happening in tech today, March 13, 2026?


The HackerNoon Newsletter brings the HackerNoon homepage straight to your inbox. On this day, Microsoft Goes Public in 1986, and we present you with these top quality stories. From 7 Iconic 20th-Century Ad Campaigns and What Todays Marketers Can Learn From Them to Milliseconds Make Millions: How and Why to Speed Up Your App, from InDrives Playbook, let’s dive right in.

HackerNoon Projects of the Week: InfoFusion Hubs, Firefox Managed Session Controller, NoteOCR


By @proofofusefulness [ 3 Min read ] Real utility over hype. Meet InfoFusion Hubs, Firefox Managed Session Controller, and NoteOCR — this weeks standout Proof of Usefulness projects. Read More.

Passkeys in Symfony 7.4: How to Build a Completely Passwordless Future


By @mattleads [ 14 Min read ] Learn to implement Apple Passkeys in Symfony 7.4. Secure, passwordless auth guide using PHP 8.x attributes and the web-auth/webauthn-symfony-bundle. Read More.

7 Iconic 20th-Century Ad Campaigns and What Todays Marketers Can Learn From Them


By @yuliiam [ 9 Min read ] Seven iconic advertising campaigns—from Volkswagen to Apple—that changed marketing forever and still offer powerful lessons for modern marketers. Read More.

Why Physical AI Must Be Superhuman


By @nishantbhanot [ 8 Min read ] Discover why Physical AI requires superhuman capability. Learn how 360° sensor fusion outperforms human limits in fog, darkness, and spatial awareness. Read More.

Milliseconds Make Millions: How and Why to Speed Up Your App, from InDrives Playbook


By @indrivetech [ 14 Min read ] How faster mobile app startup improves revenue. Practical iOS performance techniques, metrics, and real optimization results from inDrive. Read More.


🧑‍💻 What happened in your world this week?

It's been said that writing can help consolidate technical knowledge, establish credibility, and contribute to emerging community standards. Feeling stuck? We got you covered ⬇️⬇️⬇️


ANSWER THESE GREATEST INTERVIEW QUESTIONS OF ALL TIME


We hope you enjoy this worth of free reading material. Feel free to forward this email to a nerdy friend who'll love you for it.See you on Planet Internet! With love, The HackerNoon Team ✌️


HackerNoon Projects of the Week: InfoFusion Hubs, Firefox Managed Session Controller, NoteOCR

2026-03-14 00:00:17

Welcome to HackerNoon’s Projects of the Week, where we spotlight standout projects from the Proof of Usefulness Hackathon, HackerNoon’s competition designed to measure what actually matters: real utility over hype. \n \n Each week, we’ll highlight projects that demonstrate clear usefulness, technical execution, and real-world impact - backed by data, not buzzwords.

This week, we’re excited to share three projects that have proven their utility by solving concrete problems for real users: InfoFusion Hubs, Firefox Managed Session Controller, and NoteOCR.

\

:::tip Want to see your own project spotlighted here?

Join the Proof of Usefulness Hackathon to get on our radar.

:::

\

Meet the Projects of The Week

InfoFusion Hubs

https://hackernoon.com/infofusion-hubs-earns-a-14-proof-of-usefulness-score-by-building-a-multi-niche-tech-education-platform?embedable=true

\ InfoFusion Hubs is a multi-niche blogging platform covering artificial intelligence, technology, web development, SEO, and digital marketing. The platform focuses on well-researched, easy-to-understand content that helps readers stay updated with modern tech trends.

\ InfoFusion Hubs exists as the rapid evolution of AI and digital tools creates a massive need for accessible, beginner-friendly educational resources that bridge the gap between technical complexity and everyday utility.

Proof of Usefulness score: +14 / 1000

\

:::tip See InfoFusion Hubs’ full Proof of Usefulness report

Read their story on HackerNoon

:::

\

Firefox Managed Session Controller

https://hackernoon.com/firefox-managed-session-controller-earns-a-36-proof-of-usefulness-score-by-building-an-open-source-digital-wellness-tool?embedable=true

\ Firefox Managed Session Controller is an open-source technical solution designed to help parents manage internet access on Ubuntu systems. With a focus on digital wellness, this project aims to protect children from unrestricted internet exposure while allowing them to leverage technology for work and learning.

Proof of Usefulness score: +36 / 1000

\

:::tip See Firefox Managed Session Controller’s full Proof of Usefulness report

Read their story on HackerNoon

:::

\

NoteOCR

https://hackernoon.com/noteocr-earns-a-36-proof-of-usefulness-score-by-building-handwritten-note-digitization-tools?embedable=true

\ NoteOCR is a productivity tool designed to help students and professionals convert handwritten notes, tables, and scanned documents into editable digital files.

\ Using the app, users can upload handwritten notes, tabular data, and scanned documents and have them converted into editable digital documents. It is intended to assist students and professionals in reducing hours of manual typing while increasing productivity.

Proof of Usefulness score: +36 / 1000

\

:::tip See NoteOCR’s full Proof of Usefulness report

Read their story on HackerNoon

:::

\

You Too Can Get Scored

What is Proof of Usefulness?

It's our answer to a web drowning in vaporware and empty promises. We evaluate projects based on: \n ▪️ Real user adoption \n ▪️ Sustainable revenue \n ▪️ Technical stability \n ▪️ Genuine utility \n \n Projects score from -100 to +1000. Top scorers compete for $20K in cash and $130K+ in software credits.

You’ll be in good company. The hackathon is backed by teams who ship production software for a living - Bright Data, Neo4j, Storyblok, Algolia, and HackerNoon.

\

:::warning P.S. The clock is ticking - Only 3 months and 3 prize rounds remaining! Don't leave money on the table - get in early!

:::

\

\

What happens when you submit:

1. Get your free Proof of Usefulness score instantly \n 2. Your submission becomes a HackerNoon article (published within days) \n 3. Compete for monthly prizes \n 4. All participants get rewards

Complete guide on how to submit here.

\

:::tip 👉 Submit Your Project Now!

:::

\ Thanks for building useful things! \n P.S. Submissions roll monthly through June 2026. Get in early!

\ \ \ \

Passkeys in Symfony 7.4: How to Build a Completely Passwordless Future

2026-03-14 00:00:05

In the modern web era, passwords are no longer sufficient. They are the root cause of over 80% of data breaches, subject to phishing, reuse, and terrible complexity rules. The industry has spoken: Passkeys are the future.

\ Passkeys, built on the Web Authentication (WebAuthn) and FIDO2 standards, replace traditional passwords with cryptographic key pairs. Your device (iPhone, Android, Windows Hello, YubiKey) stores a private key, while the server only ever sees the public key. No hashes to steal, no passwords to reset, and inherently phishing-resistant.

\ In this comprehensive guide, we will build a 100% passwordless authentication system using Symfony and the official web-auth/webauthn-symfony-bundle. We will eliminate the concept of a password entirely from our application. No fallback, no “reset password” links. Just pure, secure, biometric-backed passkeys.

Core Architecture & Requirements

Passkeys work by replacing a shared secret (password) with a public/private key pair. The private key never leaves the user’s Apple device (iPhone, Mac, iPad), and the public key is stored on your Symfony server.

Technical Stack

  • PHP: 8.2 or higher (Required for the latest WebAuthn libs)
  • Symfony: 7.4 LTS
  • Database: PostgreSQL, MySQL, or SQLite for dev (to store Credential Sources)
  • Primary Library: web-auth/webauthn-symfony-bundle

Essential Packages

Run the following command to install the necessary dependencies:

composer require web-auth/webauthn-symfony-bundle:^5.2 \
                 web-auth/webauthn-stimulus:^5.2 \
                 symfony/uid:^7.4

\ We use @simplewebauthn/browser via AssetMapper (which provides excellent wrapper functions for the native browser WebAuthn APIs) because Apple Passkeys require a frontend interaction that is best handled via a Stimulus controller in a modern Symfony environment, or you can use React/Vue modules.

Database Schema: The Credential Source

This is where our application dramatically diverges from a traditional Symfony app. We are going to strip passwords entirely from the system.

\ Standard Symfony User entities aren’t equipped to store Passkey metadata (like AAGUIDs or public key Cose algorithms). We need a dedicated entity to store the credentials.

The User Entity

Our User entity implements Symfony\Component\Security\Core\User\UserInterface. Noticeably absent is the PasswordAuthenticatedUserInterface.

namespace App\Entity;

use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
class User implements UserInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 255, unique: true)]
    private ?string $userHandle = null;

    #[ORM\Column(length: 180, unique: true)]
    #[Assert\NotBlank]
    #[Assert\Email]
    private ?string $email = null;

    public function __construct()
    {
        $this->userHandle = Uuid::v4()->toRfc4122();
    }

    ...
}

The PublicKeyCredentialSource Entity

A single user can have multiple passkeys (e.g., Face ID on their phone, Touch ID on their Mac, a YubiKey on their keychain). We need an entity to store these public keys and their associated metadata.

\ Create src/Entity/PublicKeyCredentialSource.php. This entity must be capable of translating to and from the bundle’s native Webauthn\PublicKeyCredentialSource object.

\ Crucially, we must preserve the TrustPath. Failing to do so destroys the attestation data needed if you ever require high-security enterprise hardware keys.

namespace App\Entity;

use App\Repository\PublicKeyCredentialSourceRepository;
use Doctrine\ORM\Mapping as ORM;
use Webauthn\PublicKeyCredentialSource as WebauthnSource;

#[ORM\Entity(repositoryClass: PublicKeyCredentialSourceRepository::class)]
#[ORM\Table(name: 'webauthn_credentials')]
class PublicKeyCredentialSource extends WebauthnSource
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    public function getId(): ?int
    {
        return $this->id;
    }
}

The CredentialSourceRepository

You must also implement a CredentialSourceRepository that implements Webauthn\Bundle\Repository\PublicKeyCredentialSourceRepository.

namespace App\Repository;

use App\Entity\PublicKeyCredentialSource;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Webauthn\Bundle\Repository\PublicKeyCredentialSourceRepositoryInterface;
use Webauthn\Bundle\Repository\CanSaveCredentialSource;
use Webauthn\PublicKeyCredentialSource as WebauthnSource;
use Webauthn\PublicKeyCredentialUserEntity;

class PublicKeyCredentialSourceRepository extends ServiceEntityRepository implements PublicKeyCredentialSourceRepositoryInterface, CanSaveCredentialSource
{
    public function __construct(ManagerRegistry $registry, private readonly ObjectMapperInterface $objectMapper)
    {
        parent::__construct($registry, PublicKeyCredentialSource::class);
    }

    public function findOneByCredentialId(string $publicKeyCredentialId): ?WebauthnSource
    {
        return $this->findOneBy(['publicKeyCredentialId' => $publicKeyCredentialId]);
    }

    public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
    {
        return $this->findBy(['userHandle' => $publicKeyCredentialUserEntity->id]);
    }

    public function saveCredentialSource(WebauthnSource $publicKeyCredentialSource): void
    {
        $entity = $this->findOneBy(['publicKeyCredentialId' => base64_encode($publicKeyCredentialSource->publicKeyCredentialId)])
            ?? $this->objectMapper->map($publicKeyCredentialSource, PublicKeyCredentialSource::class);

        $this->getEntityManager()->persist($entity);
        $this->getEntityManager()->flush();
    }
}

The WebAuthn bundle relies on abstract interfaces to find and persist users and credentials. Our repositories must implement these interfaces.

The UserRepository

The UserRepository implements PublicKeyCredentialUserEntityRepositoryInterface. Because we want the bundle to handle user creation automatically during a passkey registration, we also implement CanRegisterUserEntity and CanGenerateUserEntity.

namespace App\Repository;

use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Uid\Uuid;
use Webauthn\Bundle\Repository\CanGenerateUserEntity;
use Webauthn\Bundle\Repository\CanRegisterUserEntity;
use Webauthn\Bundle\Repository\PublicKeyCredentialUserEntityRepositoryInterface;
use Webauthn\Exception\InvalidDataException;
use Webauthn\PublicKeyCredentialUserEntity;

class UserRepository extends ServiceEntityRepository implements PublicKeyCredentialUserEntityRepositoryInterface, CanRegisterUserEntity, CanGenerateUserEntity
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, User::class);
    }

    public function saveUserEntity(PublicKeyCredentialUserEntity $userEntity): void
    {
        $user = new User();
        $user->setEmail($userEntity->name);
        $user->setUserHandle($userEntity->id);

        $this->getEntityManager()->persist($user);
        $this->getEntityManager()->flush();
    }

    public function generateUserEntity(?string $username, ?string $displayName): PublicKeyCredentialUserEntity
    {
        return new PublicKeyCredentialUserEntity(
            $username ?? '',
            Uuid::v4()->toRfc4122(),
            $displayName ?? $username ?? ''
        );
    }

    ...

Configuration: Bridging Symfony and Apple

Apple requires specific “Relying Party” (RP) information. This identifies your application to the user’s iCloud Keychain.

WebAuthn Configuration

Create or update config/packages/webauthn.yaml:

webauthn:
    allowed_origins: ['%env(WEBAUTHN_ALLOWED_ORIGINS)%']
    credential_repository: 'App\Repository\PublicKeyCredentialSourceRepository'
    user_repository: 'App\Repository\UserRepository'
    creation_profiles:
        default:
            rp:
                name: '%env(RELYING_PARTY_NAME)%'
                id: '%env(RELYING_PARTY_ID)%'
    request_profiles:
        default:
            rp_id: '%env(RELYING_PARTY_ID)%'

\ WebAuthn is incredibly strict about domains. A passkey created for example.com cannot be used on phishing-example.com. To ensure our application is portable across environments, we define our Relying Party (RP) settings in the .env file.

\ Open .env or .env.local and add:

###> web-auth/webauthn-symfony-bundle ###
RELYING_PARTY_ID=localhost
RELYING_PARTY_NAME="My Application"
WEBAUTHN_ALLOWED_ORIGINS=localhost
###< web-auth/webauthn-symfony-bundle ###

In production, RELYINGPARTYID must be your exact root domain (e.g., example.com), and WebAuthn requires a secure HTTPS contextBrowsers only exempt localhost for development.

The Registration Flow (Creation)

Passkey registration is a two-step handshake:

  1. Challenge: The server generates a unique challenge and “Creation Options.”
  2. Attestation: The browser (Safari/Chrome) asks the user for FaceID/TouchID, signs the challenge, and sends the “Attestation Object” back to the server.

The Frontend: Stimulus and CSRF

Security is paramount. Even though WebAuthn is inherently phishing-resistant, your endpoints are still vulnerable to traditional Cross-Site Request Forgery (CSRF) if left unprotected. We will pass Symfony’s built-in CSRF tokens via headers in our fetch() calls.

\ Assuming you have a standard CSRF helper (like csrfprotectioncontroller.js that extracts the token from a meta tag or hidden input), we inject it into our Passkey controller.

import { Controller } from '@hotwired/stimulus';
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
import { generateCsrfHeaders } from './csrf_protection_controller.js';

export default class extends Controller {
    static values = {
        optionsUrl: String,
        resultUrl: String,
        isLogin: Boolean
    }

    connect() {
        console.log('Passkey controller connected! 🔑');
    }

    async submit(event) {
        event.preventDefault();

        const username = this.element.querySelector('[name="username"]')?.value;

        if (!this.isLoginValue && !username) {
            alert('Please provide a username/email');
            return;
        }

        const csrfHeaders = generateCsrfHeaders(this.element);

        try {
            // 1. Fetch options
            const response = await fetch(this.optionsUrlValue, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', ...csrfHeaders },
                body: username ? JSON.stringify({ username: username, displayName: username }) : '{}'
            });

            if (!response.ok) {
                const errorData = await response.json().catch(() => ({}));
                throw new Error(errorData.errorMessage || 'Failed to fetch WebAuthn options from server');
            }

            const options = await response.json();

            // 2. Trigger Apple's Passkey UI (Create or Get)
            let credential;
            if (this.isLoginValue) {
                credential = await startAuthentication({ optionsJSON: options });
            } else {
                credential = await startRegistration({ optionsJSON: options });
            }

            // 3. Send result back to verify
            const result = await fetch(this.resultUrlValue, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', ...csrfHeaders },
                body: JSON.stringify(credential)
            });

            if (result.ok) {
                window.location.reload();
            } else {
                const errorText = await result.text();
                alert('Authentication failed: ' + errorText);
            }
        } catch (e) {
            console.error(e);
            alert('WebAuthn process failed: ' + e.message);
        }
    }
}

Routing

You need to ensure the routing type for webauthn exists. Create config/routes/webauthn_routes.yaml:

webauthn_routes:
    resource: .
    type: webauthn

Security Bundle Integration

To allow users to log in with their Passkey, we need to configure the Symfony Guard (now the Authenticator system).

\ In config/packages/security.yaml:

security:
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider

            webauthn:
                authentication:
                    routes:
                        options_path: /login/passkey/options
                        result_path: /login/passkey/result
                registration:
                    enabled: true
                    routes:
                        options_path: /register/passkey/options
                        result_path: /register/passkey/result
                success_handler: App\Security\AuthenticationSuccessHandler
                failure_handler: App\Security\AuthenticationFailureHandler

            logout:
                path: app_logout
    access_control:
        - { path: ^/dashboard, roles: ROLE_USER }

The Authentication Failure Handler

Because WebAuthn ceremonies involve AJAX fetch() requests from the frontend, a standard Symfony redirect on failure (e.g., trying to register an email that already exists) will be silently swallowed by the browser, resulting in a frustrating user experience.

\ We implement a custom AuthenticationFailureHandler that returns a clean 401 Unauthorized JSON response when the request is AJAX.

\ Create src/Security/AuthenticationFailureHandler.php:

namespace App\Security;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\SecurityRequestAttributes;

readonly class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
    public function __construct(private UrlGeneratorInterface $urlGenerator) {}

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): RedirectResponse|JsonResponse
    {
        if ($request->getContentTypeFormat() === 'json' || $request->isXmlHttpRequest()) {
            return new JsonResponse([
                'status' => 'error',
                'errorMessage' => $exception->getMessageKey(),
            ], Response::HTTP_UNAUTHORIZED);
        }

        // Store the error in the session
        $request->getSession()->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, $exception);

        return new RedirectResponse($this->urlGenerator->generate('app_login'));
    }
}

The Authentication Success Handler

Since Passkeys often bypass the traditional login form, you need to define where the user goes after a successful “Handshake.”

namespace App\Security;

use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;

readonly class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    public function __construct(private UrlGeneratorInterface $urlGenerator) {}

    public function onAuthenticationSuccess(Request $request, TokenInterface $token): RedirectResponse
    {
        return new RedirectResponse($this->urlGenerator->generate('app_dashboard'));
    }
}

Verification & Apple-Specific Gotchas

  1. HTTPS is mandatory: Browsers will not expose navigator.credentials on insecure origins (except localhost).
  2. RP ID Match: Ensure the id in webauthn.yaml exactly matches your domain. If you are on dev.example.com, your RP ID should be example.com.
  3. Apple AAGUID: Apple devices often return a “Zero AAGUID” (all zeros). If your library is configured to strictly validate authenticators via metadata, you may need to allow “Unknown Authenticators” in your configuration.

Conclusion

Transitioning to Apple Passkeys with Symfony 7.4 isn’t just a security upgrade; it’s a significant improvement to your user experience. By removing the friction of password managers“forgot password” emails, and complex character requirements, you increase conversion and user retention.

\ As a senior developer or lead, your priority is ensuring that this implementation remains maintainable. By sticking to the WebAuthn-Symfony-Bundle and PHP 8.x attributes, you ensure that your codebase remains idiomatic and ready for future Symfony LTS releases.

Summary Checklist for Deployment

  • SSL/TLS: Ensure your production environment uses a valid certificate (Passkeys will fail on plain HTTP).
  • RP ID Strategy: Decide if you want to support subdomains by setting your Relaying Party ID to the top-level domain.
  • Backup Methods: Always provide a secondary login method (like Magic Links or a traditional password) for users on older devices that do not support FIDO2.
  • Metadata Validation: For high-security apps, consider enabling the web-auth/webauthn-metadata-service to verify that the Passkey is indeed coming from an Apple device and not an unauthorized emulator.

\ Source Code: You can find the full implementation and follow the project’s progress on GitHub: [https://github.com/mattleads/PasskeysAuth]

Let’s Connect!

If you found this helpful or have questions about the implementation, I’d love to hear from you. Let’s stay in touch and keep the conversation going across these platforms:

\

7 Iconic 20th-Century Ad Campaigns and What Today's Marketers Can Learn From Them

2026-03-13 23:39:10

Some ads sell products. The rare ones change culture. The 20th century gave us both kinds, and the gap between them is everything. Before social media, algorithmic targeting, and A/B testing, the campaigns that broke through did so with nothing but a sharp idea, honest copy, and a deep understanding of what people actually wanted to feel.

In 1999, Ad Age ranked the 100 greatest campaigns of the century. What's striking about that list isn't how dated it looks. It's how relevant it still is. The principles behind the best work haven't changed. Only the platforms have.

Here are 7 campaigns from that era that every marketer should know, why they worked, and what they can still teach you right now.

1. Volkswagen, "Think Small" (1959)

Agency: Doyle Dane Bernbach · Ad Age ranking: #1

Volkswagen Think Small advertisement — a tiny Beetle on a mostly white page

There is no campaign more studied, more referenced, or more deserving of the top spot. In 1959, Volkswagen needed to sell a small, odd-looking German car to an American market obsessed with size, chrome, and Cadillacs. In post-war America, that was a nearly impossible brief. And not just commercially, but culturally.

Doyle Dane Bernbach (DDB) did the opposite of everything the industry expected. Art director Helmut Krone placed a tiny photo of the Beetle in the upper left corner of a full page, surrounded by nothing but white space. Copywriter Julian Koenig wrote two words underneath: Think small.

Ad Age described DDB as having given "advertising permission to surprise, to defy and to engage the consumer without bludgeoning him." The campaign worked. According to Ad Age's historical record, DDB created six of the greatest 100 campaigns of the century, and Think Small was the crown jewel. Annual U.S. Beetle sales climbed from 120,000 units in 1959 to over 423,000 by 1968.

Why it's timeless: Truth delivered with confidence is more persuasive than any exaggerated claim. In a feed full of loud creatives and inflated promises, this lesson is more relevant now than ever.

2. Coca-Cola, "The Pause That Refreshes" (1929)

Agency: D'Arcy Co. · Ad Age ranking: #2

Coca-Cola The Pause That Refreshes vintage advertisement

Long before "emotional branding" had a name, Coca-Cola was already doing it. "The pause that refreshes" wasn't selling a drink; it was selling a moment. A feeling. A permission to stop, breathe, and enjoy something small.

The slogan was developed by Archie Lee at D'Arcy Advertising, working closely with Coca-Cola president Robert Woodruff, who — according to the Coca-Cola Company's own historical archive — believed the role of advertising was "making people like you," not selling a product. That philosophy shaped everything. Launched in 1929, as the Coca-Cola Company's advertising history confirms, the slogan became the anchor of the brand's identity through the Depression era, positioning Coca-Cola as a democratic, affordable pleasure available to everyone at any moment of the day.

According to the Coca-Cola Company's historical archive, Woodruff and Lee also commissioned artist Haddon Sundblom in 1931 to paint the now-iconic red-suited Santa Claus for Coca-Cola ads, further cementing the brand's connection to warmth, shared moments, and human feeling. Everything that followed built on this emotional foundation.

Why it's timeless: People don't buy products, they buy feelings. Coca-Cola figured that out in 1929. Every brand building emotional campaigns today is working from the same playbook.

3. Marlboro, The Marlboro Man (1955)

Agency: Leo Burnett Co. · Ad Age ranking: #3

Marlboro Man original 1955 advertisement by Leo Burnett

Marlboro was originally marketed as a women's cigarette, sold under the genteel tagline "Mild as May." Philip Morris hired Leo Burnett in November 1954 to fix that, and what Burnett created was one of the most dramatic brand repositionings in advertising history.

According to Ad Age's encyclopedia entry on Leo Burnett, the agency "took a personal role in repositioning the brand from a women's cigarette to a men's with the introduction of the Marlboro Man campaign." The first ads featured cowboys. As documented in Ad Age's tobacco marketing archive, Burnett told Philip Morris:

"The cowboy is an almost universal symbol of admired masculinity."

Research had found that smokers considered filter cigarettes "slightly effeminate," so every element was designed to counteract that perception. By 1962, Philip Morris settled on the cowboy as the exclusive Marlboro image; by 1963, he had a home: Marlboro Country.

In 1999, Ad Age named the Marlboro Man campaign the third most important of the century and the cowboy the top advertising icon of the century, one of four icons created by Leo Burnett to make the list. As Ad Age noted, no other single agency had more than one. Philip Morris itself later called Marlboro "the No. 1 trademark in the world."

Why it's timeless: People buy who they want to become, not what a product does. The Marlboro Man sold an identity so completely that it transcended the product itself.

4. Nike, "Just Do It" (1988)

Agency: Wieden+Kennedy · Ad Age ranking: #4

In 1988, Nike was losing ground to Reebok, which had dominated the aerobics boom of the mid-80s. Wieden+Kennedy co-founder Dan Wieden needed a single line to unify a series of very different TV spots, and he wrote it the night before the client presentation.

The origin is surprising. As reported by NPR in their tribute following Wieden's death in 2022, the phrase was inspired by the last words of convicted murderer Gary Gilmore before his execution: "Let's do it." Wieden changed two words and stripped it of its darkness. The first ad to carry the line featured an 80-year-old man named Walt Stack jogging across the Golden Gate Bridge — not a superstar athlete, just a person doing it. That deliberate inclusivity made the campaign speak to everyone.

As NPR documented, Nike grew its worldwide sales from $877 million in 1988 to $9.2 billion by 1998. Its share of the North American sport-shoe market climbed from 18% to 43% over the same decade.

"For some reason that line resonated deeply in the athletic community and just as deeply with people who had little or no connection to sports."

Wieden said of the response. More than 35 years later, "Just Do It" is still running.

Why it's timeless: The best slogans aren't about the product, they're about the person using it. "Just Do It" is a philosophy, not a tagline, and it speaks to something universal in human ambition.

5. De Beers, "A Diamond Is Forever" (1948)

Agency: N.W. Ayer & Son · Ad Age ranking: #6

De Beers A Diamond Is Forever vintage advertisement

Few campaigns have shaped human behaviour as profoundly as this one. Before De Beers, diamond engagement rings were not a cultural norm. Fewer than 20% of American brides owned one by the end of the 1930s, as documented in Ad Age's encyclopedia entry on De Beers.

In 1948, before a major agency presentation, N.W. Ayer copywriter Frances Gerety scribbled the line "A diamond is forever." As Ad Age reported in its De Beers encyclopedia entry, the slogan "captured both the durability of the stone and the romantic aspirations of couples entering into marriage" and immediately became the mainstay of De Beers' U.S. campaign. At the same time, N.W. Ayer developed the "Four Cs" of diamond buying — cut, color, clarity, and carat weight — framing the entire purchase category in De Beers' own language.

The numbers are staggering. By the end of the 1940s, the share of married U.S. women who owned diamond engagement rings had risen to 60%, according to Ad Age. By the 1980s it surpassed 70%. When De Beers took the campaign to Japan in 1968, a market where fewer than 5% of women received diamond engagement rings at the time, that figure reached 60% by 1981, as documented by Ad Age. De Beers was spending $200 million a year in advertising across 34 countries at peak. Ad Age later voted "A Diamond Is Forever" the most iconic advertising slogan of the 20th century.

Why it's timeless: It's the ultimate proof that advertising can create cultural norms, not just reflect them. De Beers didn't sell diamonds, they made diamonds feel necessary.

6. Avis, "We Try Harder" (1963)

Agency: Doyle Dane Bernbach · Ad Age ranking: #10

Avis We Try Harder vintage advertisement

DDB appears twice on this list because they earned it twice. "We Try Harder" was born from a brief that would have scared off almost any other agency.

In 1962, Avis had only an 11% market share and had not turned a profit in 13 years, according to Campaign magazine's historical account of the campaign. New CEO Robert Townsend called in DDB's Bill Bernbach, who demanded 90 days to learn the company before writing a word. During that deep-dive, when DDB asked whether Avis had newer cars, more locations, or lower rates than Hertz, the answer to every question was no. "Well," said Townsend, "we do try harder." That honest admission became the brief.

Copywriter Paula Green, whom Campaign described as having gone "completely against the prevailing Madison Avenue philosophy that ads must never acknowledge a brand weakness," turned it into "When you're only No. 2, you try harder. Or else." David Ogilvy later praised the campaign as "diabolical positioning," as recorded in Slate's 2013 investigation of the Hertz-Avis rivalry. Fred Danzig, then an Ad Age reporter, captured the industry reaction when the campaign launched:

"The audacity, the originality, the freshness, the life, the sassy spirit… it forever changed the way Madison Avenue communicated to the world."

The results were immediate. Within a year, Avis turned a $3.2 million loss into a $1.2 million profit, its first in over a decade, as confirmed by both Campaign and Slate. Hertz executives, Slate reported, projected that by 1968, Avis might need a new campaign because it would no longer be No. 2.

Why it's timeless: Honesty about your weaknesses, delivered with confidence, builds more trust than hollow claims of superiority. In a world of inflated promises, admitting what you're not is a surprisingly powerful differentiator.

7. Apple, "1984" (1984)

Agency: Chiat/Day · Ad Age ranking: #12

It aired once, during Super Bowl XVIII on January 22, 1984. It never ran on national television again. And it remains the most discussed commercial in advertising history.

Directed by Ridley Scott, the ad depicted a grey, dystopian world of conformity being shattered by a lone woman hurling a sledgehammer through a screen showing "Big Brother." According to Ad Age's archive of the creatives behind the spot, it was written by Steve Hayden, art directed by Brent Thomas, and creative directed by Lee Clow, with Ridley Scott brought in while he was in London on "Blade Runner." As Clow told Ad Age:

"Steve Jobs' simple challenge was, 'I think Macintosh is the greatest product in the history of the world. Make an ad that tells them that.'"

Apple used the commercial to position the Mac as the antidote to IBM's domination of the information age, two days before the computer's launch.

What makes the story richer is how close it came to never airing. Apple's board of directors hated the spot and tried to kill it. Chiat/Day executive Jay Chiat held onto the Super Bowl airtime regardless. The ad ran once and generated massive earned media far beyond anything a single TV buy could have produced. Ad Age named "1984" the Commercial of the Decade for the 1980s. As Ad Age's profile of Ridley Scott noted, the spot "effectively turned the Super Bowl into a platform for mini-blockbuster entertainment", a legacy that defines Super Bowl advertising strategy to this day.

Why it's timeless: The ad barely showed the product. It told a story about who Apple customers were — rebels, individuals, people who think differently. Forty years later, Apple still builds campaigns around that same identity.

What All 7 Have in Common

Looking across these campaigns, the patterns are impossible to miss.

None of them led with features. Not one. They led with feelings, identities, moments, and ideas. They treated their audiences as intelligent people capable of being moved, not consumers to be pushed.

They were also all built on a single, clear idea, one thought, executed with total conviction. And most importantly, they were honest. Sometimes uncomfortably so. Avis admitted they were second. De Beers built an entire campaign on a stone's one real attribute: it doesn't break. That kind of radical honesty in advertising is still rare, and still extraordinarily effective when you have the nerve to try it.

In 2026, with AI creative tools, performance dashboards, and algorithmic targeting dominating how we think about advertising, it's easy to forget that the fundamentals haven't changed. The best campaigns still earn attention rather than buy it. They still build something people want to belong to. They still tell one true thing in a way that makes people feel seen.

That's what made these campaigns timeless. And that's what will make the next great campaign timeless, too.

\

How to Build a Governance Layer for Claude Code With Hooks, Skills, and Agents

2026-03-13 23:00:58

AI assistants don’t have “bad memory.” They have bad governance.

You’ve seen it:

  • You wrote a meticulous architecture guide. The model ignores it.
  • You said “4-layer architecture.” It outputs 3 layers.
  • You said “no el-input.” It drops <el-input v-model="value" /> like it’s doing you a favor.

This isn’t about intelligence. It’s about incentives.

In Claude Code, Skills are available context, not a hard constraint. If Claude “feels” it can answer without calling a Skill, it will. And your project handbook becomes decoration.

On our team, building a RuoYi-Plus codebase with Claude Code, we tracked it:

Without intervention, Claude proactively activated project Skills only ~25% of the time.

So 3 out of 4 times, your “rules” aren’t rules. They’re a suggestion.

We wanted something stricter: a mechanism that makes Claude behave less like a clever intern and more like a staff engineer who reads the playbook before writing code.

The fix wasn’t more prompting.

The fix was Hooks.

After ~1 month of iteration, we shipped a .claude configuration stack:

  • 4 lifecycle hooks (governance)
  • 26 domain Skills (knowledge)
  • slash commands (process orchestration)
  • agents (parallel specialists)

Result:

Skill activation for dev tasks: ~25% → 90%+ Less rule-violating code, fewer “please redo it” cycles, and far fewer risky tool actions.

This article breaks down the architecture so you can reproduce it in your own repo.


1) Why Claude Code “forgets”: Skills are opt-in by default

Claude Code’s default flow looks like this:

User prompt → Claude answers (maybe calls a Skill, maybe not)

That “maybe” is the problem.

Claude’s internal decision heuristic is usually:

  • “Can I answer this quickly without extra context?”
  • not “Do I must comply with repo rules?”

So the system drifts toward convenience.

What you want is institutional friction: a lightweight “control plane” that runs before Claude starts reasoning, and shapes the work every time.


2) The breakthrough: a forced Skill evaluation hook

We implemented a hook that fires at the earliest moment: UserPromptSubmit.

It prints a short policy block that Claude sees before doing anything else:

  • Evaluate each Skill: yes/no + reason
  • If any “yes”: call Skill immediately
  • Only then: proceed to implementation

The “forced eval” hook (core logic)

We keep it deliberately dumb and deterministic:

  • detect slash commands (escape hatch)
  • otherwise print the evaluation protocol
// .claude/hooks/skill-forced-eval.js (core idea, simplified)
const prompt = process.env.CLAUDE_USER_PROMPT ?? "";
​
// Escape hatch: if user invoked a slash command, skip forced eval
const isSlash = /^\/[^\s/]+/.test(prompt.trim());
if (isSlash) process.exit(0);
​
const skills = [
 &nbsp;"crud-development",
 &nbsp;"api-development",
 &nbsp;"database-ops",
 &nbsp;"ui-pc",
 &nbsp;"ui-mobile",
 &nbsp;// ... keep going (we have 26)
];
​
const instructions = [
 &nbsp;"## Mandatory Skill Activation Protocol (MUST FOLLOW)",
 &nbsp;"",
 &nbsp;"### Step 1 — Evaluate",
 &nbsp;"For EACH skill, output: [skill] — Yes/No — Reason",
 &nbsp;"",
 &nbsp;"### Step 2 — Activate",
 &nbsp;"If ANY skill is Yes → call Skill(<name>) immediately.",
 &nbsp;"If ALL are No → state 'No skills needed' and continue.",
 &nbsp;"",
 &nbsp;"### Step 3 — Implement",
 &nbsp;"Only after Step 2 is done, start the actual solution.",
 &nbsp;"",
 &nbsp;"Available skills:",
 &nbsp;...skills.map(s => `- ${s}`)
].join("\n");
​
console.log(instructions);

What changes in practice

Before (no hook):

“Build coupon management.” Claude starts coding… and ignores your 4-layer architecture or banned components.

After (forced eval hook):

Claude must first produce an explicit decision table, then activate Skills, then implement.

The behavioral shift is dramatic because you’re eliminating “optional compliance.”


3) Why 90% and not 100%?

Because we intentionally added a fast path.

When a user knows what they want, typing a command like:

  • /dev build coupon management
  • /crud b_coupon
  • /check

should be instant. So the hook skips evaluation for slash commands and lets the command workflow take over.

That’s the tradeoff:

  • Governance for free-form prompts
  • Speed for structured commands

4) Hooks as a lifecycle control plane (the 4-hook stack)

Think of hooks as a CI pipeline for an agent session—except it runs live, in your terminal.

We use four key points:

4.1 SessionStart: “look at the repo before you talk”

When a session starts, we show:

  • current git branch
  • uncommitted changes
  • open TODOs / status
  • common commands

Example output:

🚀 Session started: RuoYi-Plus-Uniapp
Time: 2026-02-16 21:14
Branch: master
​
⚠️ Uncommitted changes: 5 files
📋 TODO: 3 open / 12 done
​
Shortcuts:
  /dev &nbsp;  build feature
  /crud &nbsp; generate module
  /check  verify conventions

Why it matters: Claude stops acting like it’s entering a blank room.


4.2 UserPromptSubmit: forced Skill evaluation (discipline)

This is the “must read the handbook” gate.


4.3 PreToolUse: the safety layer (tool governance)

Claude Code is powerful because it can run tools: Bash, write files, edit code.

That’s also how accidents happen.

PreToolUse is your last line of defense before something irreversible.

We block a small blacklist (and warn on a broader greylist):

// .claude/hooks/pre-tool-use.js (conceptual)
const cmd = process.env.CLAUDE_TOOL_INPUT ?? "";
​
const hardBlock = [
 &nbsp;/rm\s+(-rf|--recursive).*\s+\//i,
 &nbsp;/drop\s+(database|table)\b/i,
 &nbsp;/>\s*\/dev\/sd[a-z]\b/i,
];
​
if (hardBlock.some(p => p.test(cmd))) {
 &nbsp;console.log(JSON.stringify({
 &nbsp; &nbsp;decision: "block",
 &nbsp; &nbsp;reason: "Dangerous command pattern detected"
  }));
 &nbsp;process.exit(0);
}
​
// Optionally: warn/confirm on sensitive actions (mass deletes, chmod -R, etc.)

This isn’t paranoia. We’ve seen models “clean temp files” with rm -rf in the wrong directory. You want a guardrail that doesn’t rely on the model being careful.


4.4 Stop: “close the loop” with next-step guidance

When Claude finishes, we:

  • summarize file changes (new vs modified)
  • suggest next actions (run tests, docs updates, commit)
  • optionally trigger agents (code review)

Example:

✅ Done — 8 files changed
​
Next steps:
- @code-reviewer for backend conventions
- SQL changed: sync migration scripts
- Consider: git commit -m "feat: coupon module"

The goal: eliminate the “it worked in the chat” gap.


5) Skills: the knowledge layer you can actually enforce

Once activation is deterministic, Skills become what they were supposed to be: a domain-specific knowledge base.

We built 26 Skills across:

  • backend (CRUD, API, DB, annotations, error handling)
  • frontend (PC component policy, state management)
  • mobile (UI kits, uniapp platform constraints)
  • integrations (payments, WeChat, OSS)
  • quality (perf, security, bug detective)
  • engineering management (architecture, git workflow, tech decisions)

A Skill file structure that scales

Every SKILL.md follows the same skeleton:

# Skill Name
​
## When to trigger
- Keywords:
- Scenarios:
​
## Core rules
### Rule 1
Explanation + example
​
### Rule 2
Explanation + example
​
## Forbidden
- ❌ ...
​
## Reference code
- path/to/file
​
## Checklist
- [ ] ...

This consistency matters because the model learns how to consume Skills.


6) Commands: workflows, not suggestions

Skills solve “what is correct.”

Commands solve “what is the process.”

6.1 /dev: a 7-step development pipeline

We designed /dev as an opinionated workflow:

  1. clarify requirements
  2. design (API + modules + permissions)
  3. DB design (SQL + dictionaries + menus)
  4. backend (4-layer output)
  5. frontend (component policy + routing)
  6. test/verify
  7. docs/progress update

It’s basically: “how seniors want juniors to work” encoded as a runnable script.

6.2 /crud: generate a full module from a table

Input:

/crud b_coupon

Output (example set):

  • Entity / BO / VO
  • DAO + Query wrapper rules
  • Service + Controller (no forbidden patterns)
  • Mapper
  • Frontend API + page scaffold

Manual effort: 2–4 hours Command-driven: 5–10 minutes (plus review)

6.3 /check: full-stack convention linting (human-readable)

This is where we turn Skills into a verifier:

  • backend: layering rules, forbidden inheritance, query wrapper policy
  • frontend: banned components, wrapper usage
  • mobile: component kit, units, imports

7) Agents: parallel specialists (don’t overload the main thread)

Some tasks should be handled by a dedicated subagent:

  • @code-reviewer: convention checks with a strict checklist
  • @project-manager: update status docs, TODOs, progress metrics

The advantage isn’t “more intelligence.” It’s separation of concerns and reduced context pollution in the main session.

A practical pattern:

  1. main agent generates code
  2. Stop hook suggests review
  3. you invoke @code-reviewer
  4. reviewer returns a structured diff + fixes list

8) The full system: governance + knowledge + process + division of labor

This is the architecture in one sentence:

Hooks enforce behavior, Skills provide standards, Commands encode workflows, Agents handle parallel expertise.

And yes—this is how you turn a “general AI assistant” into a “repo-native teammate.”


9) Minimal reproducible setup (copy-paste friendly)

If you want the smallest version that still works, build this:

.claude/
  settings.json
  hooks/
 &nbsp;  skill-forced-eval.js
 &nbsp;  pre-tool-use.js
  skills/
 &nbsp;  crud-development/
 &nbsp; &nbsp;  SKILL.md

Minimal settings.json (UserPromptSubmit hook)

{
 &nbsp;"hooks": {
 &nbsp; &nbsp;"UserPromptSubmit": [
 &nbsp; &nbsp;  {
 &nbsp; &nbsp; &nbsp; &nbsp;"matcher": "",
 &nbsp; &nbsp; &nbsp; &nbsp;"hooks": [
 &nbsp; &nbsp; &nbsp; &nbsp;  {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"type": "command",
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"command": "node .claude/hooks/skill-forced-eval.js"
 &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  ]
 &nbsp; &nbsp;  }
 &nbsp;  ],
 &nbsp; &nbsp;"PreToolUse": [
 &nbsp; &nbsp;  {
 &nbsp; &nbsp; &nbsp; &nbsp;"matcher": "",
 &nbsp; &nbsp; &nbsp; &nbsp;"hooks": [
 &nbsp; &nbsp; &nbsp; &nbsp;  {
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"type": "command",
 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"command": "node .claude/hooks/pre-tool-use.js"
 &nbsp; &nbsp; &nbsp; &nbsp;  }
 &nbsp; &nbsp; &nbsp;  ]
 &nbsp; &nbsp;  }
 &nbsp;  ]
  }
}

Then iterate:

  • add SessionStart
  • add Stop
  • grow Skills only when you catch repeated “wrong behavior”
  • encode repeat workflows as commands
  • offload review/PM tasks to agents

Closing: “ability” isn’t the bottleneck — activation is

Your model is already capable.

What’s missing is a system that makes the right behavior automatic.

A smart new hire without a handbook will freestyle. A smart new hire with:

  • enforced checklists
  • clear forbidden patterns
  • repeatable workflows
  • specialists for reviews and PM

…becomes consistent fast.

Claude is the same.

\