Skip to content
C.W.K.
Lesson 03 of 05 · published

Incognito

~13 min · webkit, privacy, incognito, data-store

Level 0New Tab
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

An identity that is in no book

Incognito in Voyager is not a separate mode bolted onto the browser. It is one more identity, with two differences. Its id is fixed, 00000000-0000-4000-8000-0000000000F1, and it appears in no IdentityBook; its comment says "Nothing of it is written anywhere." And its store is not WKWebsiteDataStore(forIdentifier:) but WKWebsiteDataStore.nonPersistent(), which Apple describes as a store that keeps website data in memory and "doesn't write that data to disk", adding that you use it "to implement private browsing".

Because Incognito is an identity, everything the last two lessons built applies to it for free. A pop-up from a private page is built from the private configuration, a link opened in a new tab takes the private identity, and nothing moves cookies in or out.

A private window

A BrowserWindowModel is made private or not, once. In a private window every tab is in the private identity, whatever the caller asked for: openTab ignores the identity argument there. The test privateWindowsCannotImportAnAccountOrRestoreHistory opens a private tab while explicitly naming the default identity and gets .privateSession anyway.

You can tell such a window at a glance. A purple Incognito label stays in the toolbar even while the address is being edited, and a new private window opens on a local explanation instead of the Home, so it never contacts the family hub for its catalog.

Then come the writes that do not happen. Each one is a guard on isPrivate in the parent or on the private id in the child:

  • No history: noteVisit returns before recording.
  • No session: the window's snapshot holds no tabs, a restore opens one fresh empty tab, and a closed private tab cannot be reopened.
  • No passwords: no offer to save a login, and a server's sign-in request is not answered from the saved ones.
  • No account shortcut can name the private identity (lesson 4).
  • No extensions: a private page's configuration gets no extension controller, and no extension may list or focus a private window.
  • No Pippa: her panel does not open, and every request for page context gets "Page sharing is off in Incognito." The toolbar says "Pippa is off in Incognito".
  • No cookie leaves: the engine answers the parent's one cookie question with nil for the private identity, and never reports that its cookies changed (lesson 5).

The daily UX review adds the edges: favicons for private pages are cached in memory only, and private downloads are hidden from the list when the session ends. A downloaded file and a bookmark Dad saves on purpose remain, and a download already under way may finish.

One private store, dropped at the last window

All private windows share one ephemeral store, as Chrome's Incognito windows share one session. The start page says it to Dad in two sentences: "Tabs share a temporary session, separate from your normal accounts. Close every Incognito window to start fresh." The test closingThePrivateSessionResetsOnlyItsCookiesAndStorage pins it: two private pages see the same store object, a value written in one shows up in the other, and a normal page sees neither.

When the last private window closes, the app delegate calls endPrivateSession(). The contract asks the engine to "Drop only the ephemeral session's store synchronously, before another private window can open." The WebKit child does it by removing the private entry from its dictionary of stores, in a plain synchronous call. The next private page asks for a store, finds none, and is given a brand-new non-persistent one. The test checks exactly that: after endPrivateSession() a fresh private page has a different store and sees null|, while the normal page still reads normal|fixture=normal. Only the private session resets.

Crossing the line

A tab can be dragged from one window to another, and that is the one kind of move that could carry state across the private boundary. The shell never offers that crossing: a dragged tab lands only in a window of its own kind, because the window it drops on is found by matching isPrivate, and Move Tab to New Window opens a window of the same kind too. attach guards the line anyway, in the model, so no future caller can cross it by accident. There, a tab entering a private window would take the private identity, and a private tab entering a normal window would take the default identity. Either way it would keep its address and title and lose its restorable state, since that state belonged to another store. The comment says it in a sentence: "A cross-privacy move cannot import another store's history or sign-in state."

What it does not claim

The review is explicit: "Incognito does not hide network traffic from websites or network operators." Private means this Mac keeps nothing of the session. Sites still see the visit, and so does the network in between.

Code

cwkVoyager, Sources/VoyagerWebKit/WebKitEngine.swift: removing a persistent store, and dropping the private one (verbatim, lines 141-153)·swift
    /// Removes an identity's cookies and site storage for good. No page of
    /// that identity may still be open.
    public func removeData(of identity: IdentityID) async throws {
        if let watcher = cookieWatchers.removeValue(forKey: identity) { stores[identity]?.httpCookieStore.remove(watcher) }
        stores[identity] = nil
        guard identity != .privateSession else { return }
        try await WKWebsiteDataStore.remove(forIdentifier: identity.raw)
    }

    public func endPrivateSession() {
        stores[.privateSession] = nil
        downloads.endPrivateSession()
    }
cwkVoyager, Sources/VoyagerCore/BrowserWindowModel.swift: a moved tab cannot carry state across the private line (verbatim, lines 398-413)·swift
    /// Puts a tab taken from another window in at the end.
    public func attach(_ saved: SessionTab) {
        // Moving a tab preserves its Sidekick key. A cross-privacy move cannot
        // import another store's history or sign-in state.
        let identity = isPrivate ? IdentityID.privateSession
            : (saved.identity == .privateSession ? services.defaultIdentity : saved.identity)
        let tab = BrowserTab(
            id: TabID(saved.id), identity: identity,
            status: PageStatus(url: saved.url, title: saved.title, favicon: saved.favicon),
            restorable: identity == saved.identity ? saved.state : nil, pinned: false)
        tab.sharesContextWithPippa = saved.sharesContextWithPippa
        tab.window = self
        tabs.append(tab)
        select(tab.id)
        sessionChanged()
    }
cwkVoyager, Tests/VoyagerWebKitTests/WebKitPageTests.swift: ending the private session resets only its store (verbatim, lines 305-333)·swift
    @Test func closingThePrivateSessionResetsOnlyItsCookiesAndStorage() async throws {
        let normalID = IdentityID()
        func exercise() async throws {
            let normal = makePage(identity: normalID)
            let first = makePage(identity: .privateSession)
            let second = makePage(identity: .privateSession)
            #expect(!first.webView.configuration.websiteDataStore.isPersistent)
            #expect(first.webView.configuration.websiteDataStore === second.webView.configuration.websiteDataStore)
            #expect(normal.webView.configuration.websiteDataStore !== first.webView.configuration.websiteDataStore)
            for page in [normal, first, second] { await secure(page, "<p>private store fixture</p>") }
            _ = await js(normal, "localStorage.setItem('fixture', 'normal'); document.cookie = 'fixture=normal; path=/'; return 1")
            #expect(await js(first, "return String(localStorage.getItem('fixture')) + '|' + document.cookie") == "null|")
            _ = await js(first, "localStorage.setItem('fixture', 'private'); document.cookie = 'fixture=private; path=/'; return 1")
            var shared = ""
            for _ in 0..<60 {
                shared = await js(second, "return String(localStorage.getItem('fixture')) + '|' + document.cookie")
                if shared == "private|fixture=private" { break }
                try await Task.sleep(for: .milliseconds(50))
            }
            #expect(shared == "private|fixture=private")
            first.close(); second.close()
            engine.endPrivateSession()
            let fresh = makePage(identity: .privateSession)
            await secure(fresh, "<p>fresh private session</p>")
            #expect(fresh.webView.configuration.websiteDataStore !== first.webView.configuration.websiteDataStore)
            #expect(await js(fresh, "return String(localStorage.getItem('fixture')) + '|' + document.cookie") == "null|")
            #expect(await js(normal, "return String(localStorage.getItem('fixture')) + '|' + document.cookie") == "normal|fixture=normal")
            fresh.close(); normal.close()
        }

External links

Exercise

Add a private mode to an app that shows web content. Back it with WKWebsiteDataStore.nonPersistent() shared by every private view, and drop your reference to that store when the last private view closes. Then list every place your app writes something because of a page (history, a restored session, a recently-closed list, saved passwords, a thumbnail or icon cache, logs, an assistant or extension that reads the page) and give each one a guard and a test that opens a private page and proves nothing was written.
Hint
Grep for every write to disk and every call that sends page data elsewhere, then ask of each one: can a private page reach this? Do not forget moves: dragging a tab, or restoring a closed one, is a path by which state can cross from one store to another.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.